Merge branch 'profile-fields' into next
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 15 Jan 2011 14:48:28 +0000 (15:48 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sat, 15 Jan 2011 14:48:28 +0000 (15:48 +0100)
28 files changed:
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/core/SoneDownloader.java
src/main/java/net/pterodactylus/sone/data/Fingerprintable.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/data/Profile.java
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/template/JavascriptFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/DeleteProfileFieldPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/EditProfileFieldPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/EditProfilePage.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/DismissNotificationAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetTranslationPage.java
src/main/java/net/pterodactylus/sone/web/ajax/JsonPage.java
src/main/java/net/pterodactylus/sone/web/ajax/LockSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/UnlockSoneAjaxPage.java
src/main/resources/i18n/sone.en.properties
src/main/resources/static/css/sone.css
src/main/resources/static/javascript/sone.js
src/main/resources/templates/deleteProfileField.html [new file with mode: 0644]
src/main/resources/templates/editProfile.html
src/main/resources/templates/editProfileField.html [new file with mode: 0644]
src/main/resources/templates/insert/sone.xml
src/main/resources/templates/invalid.html [new file with mode: 0644]
src/main/resources/templates/viewSone.html

index 78df9ef..a1282cc 100644 (file)
@@ -34,6 +34,7 @@ import net.pterodactylus.sone.core.Options.OptionWatcher;
 import net.pterodactylus.sone.data.Client;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.freenet.wot.Identity;
@@ -1033,6 +1034,17 @@ public class Core implements IdentityListener, UpdateListener {
                profile.setBirthMonth(configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").getValue(null));
                profile.setBirthYear(configuration.getIntValue(sonePrefix + "/Profile/BirthYear").getValue(null));
 
+               /* load profile fields. */
+               while (true) {
+                       String fieldPrefix = sonePrefix + "/Profile/Fields/" + profile.getFields().size();
+                       String fieldName = configuration.getStringValue(fieldPrefix + "/Name").getValue(null);
+                       if (fieldName == null) {
+                               break;
+                       }
+                       String fieldValue = configuration.getStringValue(fieldPrefix + "/Value").getValue("");
+                       profile.addField(fieldName).setValue(fieldValue);
+               }
+
                /* load posts. */
                Set<Post> posts = new HashSet<Post>();
                while (true) {
@@ -1165,6 +1177,15 @@ public class Core implements IdentityListener, UpdateListener {
                        configuration.getIntValue(sonePrefix + "/Profile/BirthMonth").setValue(profile.getBirthMonth());
                        configuration.getIntValue(sonePrefix + "/Profile/BirthYear").setValue(profile.getBirthYear());
 
+                       /* save profile fields. */
+                       int fieldCounter = 0;
+                       for (Field profileField : profile.getFields()) {
+                               String fieldPrefix = sonePrefix + "/Profile/Fields/" + fieldCounter++;
+                               configuration.getStringValue(fieldPrefix + "/Name").setValue(profileField.getName());
+                               configuration.getStringValue(fieldPrefix + "/Value").setValue(profileField.getValue());
+                       }
+                       configuration.getStringValue(sonePrefix + "/Profile/Fields/" + fieldCounter + "/Name").setValue(null);
+
                        /* save posts. */
                        int postCounter = 0;
                        for (Post post : sone.getPosts()) {
index 4328f02..af4155d 100644 (file)
@@ -310,6 +310,25 @@ public class SoneDownloader extends AbstractService {
                Profile profile = new Profile().setFirstName(profileFirstName).setMiddleName(profileMiddleName).setLastName(profileLastName);
                profile.setBirthDay(profileBirthDay).setBirthMonth(profileBirthMonth).setBirthYear(profileBirthYear);
 
+               /* parse profile fields. */
+               SimpleXML profileFieldsXml = profileXml.getNode("fields");
+               if (profileFieldsXml != null) {
+                       for (SimpleXML fieldXml : profileFieldsXml.getNodes("field")) {
+                               String fieldName = fieldXml.getValue("field-name", null);
+                               String fieldValue = fieldXml.getValue("field-value", null);
+                               if ((fieldName == null) || (fieldValue == null)) {
+                                       logger.log(Level.WARNING, "Downloaded profile field for Sone %s with missing data! Name: %s, Value: %s", new Object[] { sone, fieldName, fieldValue });
+                                       return null;
+                               }
+                               try {
+                                       profile.addField(fieldName).setValue(fieldValue);
+                               } catch (IllegalArgumentException iae1) {
+                                       logger.log(Level.WARNING, "Duplicate field: " + fieldName, iae1);
+                                       return null;
+                               }
+                       }
+               }
+
                /* parse posts. */
                SimpleXML postsXml = soneXml.getNode("posts");
                Set<Post> posts = new HashSet<Post>();
diff --git a/src/main/java/net/pterodactylus/sone/data/Fingerprintable.java b/src/main/java/net/pterodactylus/sone/data/Fingerprintable.java
new file mode 100644 (file)
index 0000000..013e0b3
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Sone - Fingerprintable.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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/>.
+ */
+
+package net.pterodactylus.sone.data;
+
+/**
+ * Interface for objects that can create a fingerprint of themselves, e.g. to
+ * detect modifications. The fingerprint should only contain original
+ * information; derived information should not be included.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface Fingerprintable {
+
+       /**
+        * Returns the fingerprint of this object.
+        *
+        * @return The fingerprint of this object
+        */
+       public String getFingerprint();
+
+}
index 7c29430..8d4306d 100644 (file)
 
 package net.pterodactylus.sone.data;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+import net.pterodactylus.util.validation.Validation;
+
 /**
  * A profile stores personal information about a {@link Sone}. All information
  * is optional and can be {@code null}.
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Profile {
-
-       /** Whether the profile was modified. */
-       private volatile boolean modified;
+public class Profile implements Fingerprintable {
 
        /** The first name. */
        private volatile String firstName;
@@ -46,6 +50,9 @@ public class Profile {
        /** The year of the birth date. */
        private volatile Integer birthYear;
 
+       /** Additional fields in the profile. */
+       private final List<Field> fields = Collections.synchronizedList(new ArrayList<Field>());
+
        /**
         * Creates a new empty profile.
         */
@@ -69,6 +76,7 @@ public class Profile {
                this.birthDay = profile.birthDay;
                this.birthMonth = profile.birthMonth;
                this.birthYear = profile.birthYear;
+               this.fields.addAll(profile.fields);
        }
 
        //
@@ -76,18 +84,6 @@ public class Profile {
        //
 
        /**
-        * Returns whether this profile was modified after creation. To clear the
-        * “is modified” flag you need to create a new profile from this one using
-        * the {@link #Profile(Profile)} constructor.
-        *
-        * @return {@code true} if this profile was modified after creation,
-        *         {@code false} otherwise
-        */
-       public boolean isModified() {
-               return modified;
-       }
-
-       /**
         * Returns the first name.
         *
         * @return The first name
@@ -104,7 +100,6 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setFirstName(String firstName) {
-               modified |= ((firstName != null) && (!firstName.equals(this.firstName))) || (this.firstName != null);
                this.firstName = firstName;
                return this;
        }
@@ -126,7 +121,6 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setMiddleName(String middleName) {
-               modified |= ((middleName != null) && (!middleName.equals(this.middleName))) || (this.middleName != null);
                this.middleName = middleName;
                return this;
        }
@@ -148,7 +142,6 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setLastName(String lastName) {
-               modified |= ((lastName != null) && (!lastName.equals(this.lastName))) || (this.lastName != null);
                this.lastName = lastName;
                return this;
        }
@@ -170,7 +163,6 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setBirthDay(Integer birthDay) {
-               modified |= ((birthDay != null) && (!birthDay.equals(this.birthDay))) || (this.birthDay != null);
                this.birthDay = birthDay;
                return this;
        }
@@ -192,7 +184,6 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setBirthMonth(Integer birthMonth) {
-               modified |= ((birthMonth != null) && (!birthMonth.equals(this.birthMonth))) || (this.birthMonth != null);
                this.birthMonth = birthMonth;
                return this;
        }
@@ -214,9 +205,292 @@ public class Profile {
         * @return This profile (for method chaining)
         */
        public Profile setBirthYear(Integer birthYear) {
-               modified |= ((birthYear != null) && (!birthYear.equals(this.birthYear))) || (this.birthYear != null);
                this.birthYear = birthYear;
                return this;
        }
 
+       /**
+        * Returns the fields of this profile.
+        *
+        * @return The fields of this profile
+        */
+       public List<Field> getFields() {
+               return new ArrayList<Field>(fields);
+       }
+
+       /**
+        * Returns whether this profile contains the given field.
+        *
+        * @param field
+        *            The field to check for
+        * @return {@code true} if this profile contains the field, false otherwise
+        */
+       public boolean hasField(Field field) {
+               return fields.contains(field);
+       }
+
+       /**
+        * Returns the field with the given ID.
+        *
+        * @param fieldId
+        *            The ID of the field to get
+        * @return The field, or {@code null} if this profile does not contain a
+        *         field with the given ID
+        */
+       public Field getFieldById(String fieldId) {
+               Validation.begin().isNotNull("Field ID", fieldId).check();
+               for (Field field : fields) {
+                       if (field.getId().equals(fieldId)) {
+                               return field;
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * Returns the field with the given name.
+        *
+        * @param fieldName
+        *            The name of the field to get
+        * @return The field, or {@code null} if this profile does not contain a
+        *         field with the given name
+        */
+       public Field getFieldByName(String fieldName) {
+               for (Field field : fields) {
+                       if (field.getName().equals(fieldName)) {
+                               return field;
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * Appends a new field to the list of fields.
+        *
+        * @param fieldName
+        *            The name of the new field
+        * @return The new field
+        * @throws IllegalArgumentException
+        *             if the name is not valid
+        */
+       public Field addField(String fieldName) throws IllegalArgumentException {
+               Validation.begin().isNotNull("Field Name", fieldName).check().isGreater("Field Name Length", fieldName.length(), 0).isNull("Field Name Unique", getFieldByName(fieldName)).check();
+               @SuppressWarnings("synthetic-access")
+               Field field = new Field().setName(fieldName);
+               fields.add(field);
+               return field;
+       }
+
+       /**
+        * Moves the given field up one position in the field list. The index of the
+        * field to move must be greater than {@code 0} (because you obviously can
+        * not move the first field further up).
+        *
+        * @param field
+        *            The field to move up
+        */
+       public void moveFieldUp(Field field) {
+               Validation.begin().isNotNull("Field", field).check().is("Field Existing", hasField(field)).isGreater("Field Index", getFieldIndex(field), 0).check();
+               int fieldIndex = getFieldIndex(field);
+               fields.remove(field);
+               fields.add(fieldIndex - 1, field);
+       }
+
+       /**
+        * Moves the given field down one position in the field list. The index of
+        * the field to move must be less than the index of the last field (because
+        * you obviously can not move the last field further down).
+        *
+        * @param field
+        *            The field to move down
+        */
+       public void moveFieldDown(Field field) {
+               Validation.begin().isNotNull("Field", field).check().is("Field Existing", hasField(field)).isLess("Field Index", getFieldIndex(field), fields.size() - 1).check();
+               int fieldIndex = getFieldIndex(field);
+               fields.remove(field);
+               fields.add(fieldIndex + 1, field);
+       }
+
+       /**
+        * Removes the given field.
+        *
+        * @param field
+        *            The field to remove
+        */
+       public void removeField(Field field) {
+               Validation.begin().isNotNull("Field", field).check().is("Field Existing", hasField(field)).check();
+               fields.remove(field);
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Returns the index of the field with the given name.
+        *
+        * @param field
+        *            The name of the field
+        * @return The index of the field, or {@code -1} if there is no field with
+        *         the given name
+        */
+       private int getFieldIndex(Field field) {
+               return fields.indexOf(field);
+       }
+
+       //
+       // INTERFACE Fingerprintable
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getFingerprint() {
+               StringBuilder fingerprint = new StringBuilder();
+               fingerprint.append("Profile(");
+               if (firstName != null) {
+                       fingerprint.append("FirstName(").append(firstName).append(')');
+               }
+               if (middleName != null) {
+                       fingerprint.append("MiddleName(").append(middleName).append(')');
+               }
+               if (lastName != null) {
+                       fingerprint.append("LastName(").append(lastName).append(')');
+               }
+               if (birthDay != null) {
+                       fingerprint.append("BirthDay(").append(birthDay).append(')');
+               }
+               if (birthMonth != null) {
+                       fingerprint.append("BirthMonth(").append(birthMonth).append(')');
+               }
+               if (birthYear != null) {
+                       fingerprint.append("BirthYear(").append(birthYear).append(')');
+               }
+               fingerprint.append("ContactInformation(");
+               for (Field field : fields) {
+                       fingerprint.append(field.getName()).append('(').append(field.getValue()).append(')');
+               }
+               fingerprint.append(")");
+               fingerprint.append(")");
+
+               return fingerprint.toString();
+       }
+
+       /**
+        * Container for a profile field.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public class Field {
+
+               /** The ID of the field. */
+               private final String id;
+
+               /** The name of the field. */
+               private String name;
+
+               /** The value of the field. */
+               private String value;
+
+               /**
+                * Creates a new field with a random ID.
+                */
+               private Field() {
+                       this(UUID.randomUUID().toString());
+               }
+
+               /**
+                * Creates a new field with the given ID.
+                *
+                * @param id
+                *            The ID of the field
+                */
+               private Field(String id) {
+                       Validation.begin().isNotNull("Field ID", id).check();
+                       this.id = id;
+               }
+
+               /**
+                * Returns the ID of this field.
+                *
+                * @return The ID of this field
+                */
+               public String getId() {
+                       return id;
+               }
+
+               /**
+                * Returns the name of this field.
+                *
+                * @return The name of this field
+                */
+               public String getName() {
+                       return name;
+               }
+
+               /**
+                * Sets the name of this field. The name must not be {@code null} and
+                * must not match any other fields in this profile but my match the name
+                * of this field.
+                *
+                * @param name
+                *            The new name of this field
+                * @return This field
+                */
+               public Field setName(String name) {
+                       Validation.begin().isNotNull("Field Name", name).check().is("Field Unique", (getFieldByName(name) == null) || equals(getFieldByName(name))).check();
+                       this.name = name;
+                       return this;
+               }
+
+               /**
+                * Returns the value of this field.
+                *
+                * @return The value of this field
+                */
+               public String getValue() {
+                       return value;
+               }
+
+               /**
+                * Sets the value of this field. While {@code null} is allowed, no
+                * guarantees are made that {@code null} values are correctly persisted
+                * across restarts of the plugin!
+                *
+                * @param value
+                *            The new value of this field
+                * @return This field
+                */
+               public Field setValue(String value) {
+                       this.value = value;
+                       return this;
+               }
+
+               //
+               // OBJECT METHODS
+               //
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public boolean equals(Object object) {
+                       if (!(object instanceof Field)) {
+                               return false;
+                       }
+                       Field field = (Field) object;
+                       return id.equals(field.id);
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public int hashCode() {
+                       return id.hashCode();
+               }
+
+       }
+
 }
index 7d4f7ef..03dff75 100644 (file)
@@ -40,7 +40,7 @@ import freenet.keys.FreenetURI;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Sone {
+public class Sone implements Fingerprintable {
 
        /** comparator that sorts Sones by their nice name. */
        public static final Comparator<Sone> NICE_NAME_COMPARATOR = new Comparator<Sone>() {
@@ -580,36 +580,17 @@ public class Sone {
                return this;
        }
 
+       //
+       // FINGERPRINTABLE METHODS
+       //
+
        /**
-        * Returns a fingerprint of this Sone. The fingerprint only depends on data
-        * that is actually stored when a Sone is inserted. The fingerprint can be
-        * used to detect changes in Sone data and can also be used to detect if
-        * previous changes are reverted.
-        *
-        * @return The fingerprint of this Sone
+        * {@inheritDoc}
         */
+       @Override
        public synchronized String getFingerprint() {
                StringBuilder fingerprint = new StringBuilder();
-               fingerprint.append("Profile(");
-               if (profile.getFirstName() != null) {
-                       fingerprint.append("FirstName(").append(profile.getFirstName()).append(')');
-               }
-               if (profile.getMiddleName() != null) {
-                       fingerprint.append("MiddleName(").append(profile.getMiddleName()).append(')');
-               }
-               if (profile.getLastName() != null) {
-                       fingerprint.append("LastName(").append(profile.getLastName()).append(')');
-               }
-               if (profile.getBirthDay() != null) {
-                       fingerprint.append("BirthDay(").append(profile.getBirthDay()).append(')');
-               }
-               if (profile.getBirthMonth() != null) {
-                       fingerprint.append("BirthMonth(").append(profile.getBirthMonth()).append(')');
-               }
-               if (profile.getBirthYear() != null) {
-                       fingerprint.append("BirthYear(").append(profile.getBirthYear()).append(')');
-               }
-               fingerprint.append(")");
+               fingerprint.append(profile.getFingerprint());
 
                fingerprint.append("Posts(");
                for (Post post : getPosts()) {
diff --git a/src/main/java/net/pterodactylus/sone/template/JavascriptFilter.java b/src/main/java/net/pterodactylus/sone/template/JavascriptFilter.java
new file mode 100644 (file)
index 0000000..0cd55de
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Sone - JavascriptFilter.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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/>.
+ */
+
+package net.pterodactylus.sone.template;
+
+import java.util.Map;
+
+import net.pterodactylus.util.number.Hex;
+import net.pterodactylus.util.template.DataProvider;
+import net.pterodactylus.util.template.Filter;
+
+/**
+ * Escapes double quotes, backslashes, carriage returns and line feeds, and
+ * additionally encloses a given string with double quotes to make it possible
+ * to use a string in Javascript.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class JavascriptFilter implements Filter {
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Object format(DataProvider dataProvider, Object data, Map<String, String> parameters) {
+               StringBuilder javascriptString = new StringBuilder();
+               javascriptString.append('"');
+               for (char c : String.valueOf(data).toCharArray()) {
+                       if (c == '\r') {
+                               javascriptString.append("\\r");
+                               continue;
+                       }
+                       if (c == '\n') {
+                               javascriptString.append("\\n");
+                               continue;
+                       }
+                       if (c == '\t') {
+                               javascriptString.append("\\t");
+                               continue;
+                       }
+                       if ((c == '"') || (c == '\\')) {
+                               javascriptString.append('\\');
+                               javascriptString.append(c);
+                       } else if (c < 32) {
+                               javascriptString.append("\\x").append(Hex.toHex((byte) c));
+                       } else {
+                               javascriptString.append(c);
+                       }
+               }
+               javascriptString.append('"');
+               return javascriptString.toString();
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/DeleteProfileFieldPage.java b/src/main/java/net/pterodactylus/sone/web/DeleteProfileFieldPage.java
new file mode 100644 (file)
index 0000000..9b52b04
--- /dev/null
@@ -0,0 +1,84 @@
+/*
+ * Sone - DeleteProfileFieldPage.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.DataProvider;
+import net.pterodactylus.util.template.Template;
+
+/**
+ * Page that lets the user confirm the deletion of a profile field.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DeleteProfileFieldPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “delete profile field” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public DeleteProfileFieldPage(Template template, WebInterface webInterface) {
+               super("deleteProfileField.html", template, "Page.DeleteProfileField.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
+               Sone currentSone = getCurrentSone(request.getToadletContext());
+               Profile profile = currentSone.getProfile();
+
+               /* get parameters from request. */
+               String fieldId = request.getHttpRequest().getParam("field");
+               Field field = profile.getFieldById(fieldId);
+               if (field == null) {
+                       throw new RedirectException("invalid.html");
+               }
+
+               /* process POST request. */
+               if (request.getMethod() == Method.POST) {
+                       if (request.getHttpRequest().getPartAsStringFailsafe("confirm", 4).equals("true")) {
+                               fieldId = request.getHttpRequest().getParam("field");
+                               field = profile.getFieldById(fieldId);
+                               if (field == null) {
+                                       throw new RedirectException("invalid.html");
+                               }
+                               profile.removeField(field);
+                               currentSone.setProfile(profile);
+                       }
+                       throw new RedirectException("editProfile.html#profile-fields");
+               }
+
+               /* set current values in template. */
+               dataProvider.set("field", field);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/EditProfileFieldPage.java b/src/main/java/net/pterodactylus/sone/web/EditProfileFieldPage.java
new file mode 100644 (file)
index 0000000..1b64b08
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * Sone - EditProfileFieldPage.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.Page.Request.Method;
+import net.pterodactylus.util.template.DataProvider;
+import net.pterodactylus.util.template.Template;
+
+/**
+ * Page that lets the user edit the name of a profile field.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditProfileFieldPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “edit profile field” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public EditProfileFieldPage(Template template, WebInterface webInterface) {
+               super("editProfileField.html", template, "Page.EditProfileField.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(Request request, DataProvider dataProvider) throws RedirectException {
+               super.processTemplate(request, dataProvider);
+               Sone currentSone = getCurrentSone(request.getToadletContext());
+               Profile profile = currentSone.getProfile();
+
+               /* get parameters from request. */
+               String fieldId = request.getHttpRequest().getParam("field");
+               Field field = profile.getFieldById(fieldId);
+               if (field == null) {
+                       throw new RedirectException("invalid.html");
+               }
+
+               /* process the POST request. */
+               if (request.getMethod() == Method.POST) {
+                       if (request.getHttpRequest().getPartAsStringFailsafe("cancel", 4).equals("true")) {
+                               throw new RedirectException("editProfile.html#profile-fields");
+                       }
+                       fieldId = request.getHttpRequest().getPartAsStringFailsafe("field", 36);
+                       field = profile.getFieldById(fieldId);
+                       if (field == null) {
+                               throw new RedirectException("invalid.html");
+                       }
+                       String name = request.getHttpRequest().getPartAsStringFailsafe("name", 256);
+                       Field existingField = profile.getFieldByName(name);
+                       if ((existingField == null) || (existingField.equals(field))) {
+                               field.setName(name);
+                               currentSone.setProfile(profile);
+                               throw new RedirectException("editProfile.html#profile-fields");
+                       }
+                       dataProvider.set("duplicateFieldName", true);
+               }
+
+               /* store current values in template. */
+               dataProvider.set("field", field);
+       }
+
+}
index 01d4815..380e054 100644 (file)
 
 package net.pterodactylus.sone.web;
 
+import java.util.List;
+
 import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.page.Page.Request.Method;
 import net.pterodactylus.util.number.Numbers;
@@ -63,21 +66,68 @@ public class EditProfilePage extends SoneTemplatePage {
                Integer birthDay = profile.getBirthDay();
                Integer birthMonth = profile.getBirthMonth();
                Integer birthYear = profile.getBirthYear();
+               List<Field> fields = profile.getFields();
                if (request.getMethod() == Method.POST) {
-                       firstName = request.getHttpRequest().getPartAsStringFailsafe("first-name", 256).trim();
-                       middleName = request.getHttpRequest().getPartAsStringFailsafe("middle-name", 256).trim();
-                       lastName = request.getHttpRequest().getPartAsStringFailsafe("last-name", 256).trim();
-                       birthDay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-day", 256).trim());
-                       birthMonth = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-month", 256).trim());
-                       birthYear = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-year", 256).trim());
-                       profile.setFirstName(firstName.length() > 0 ? firstName : null);
-                       profile.setMiddleName(middleName.length() > 0 ? middleName : null);
-                       profile.setLastName(lastName.length() > 0 ? lastName : null);
-                       profile.setBirthDay(birthDay).setBirthMonth(birthMonth).setBirthYear(birthYear);
-                       if (profile.isModified()) {
+                       if (request.getHttpRequest().getPartAsStringFailsafe("save-profile", 4).equals("true")) {
+                               firstName = request.getHttpRequest().getPartAsStringFailsafe("first-name", 256).trim();
+                               middleName = request.getHttpRequest().getPartAsStringFailsafe("middle-name", 256).trim();
+                               lastName = request.getHttpRequest().getPartAsStringFailsafe("last-name", 256).trim();
+                               birthDay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-day", 256).trim());
+                               birthMonth = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-month", 256).trim());
+                               birthYear = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("birth-year", 256).trim());
+                               profile.setFirstName(firstName.length() > 0 ? firstName : null);
+                               profile.setMiddleName(middleName.length() > 0 ? middleName : null);
+                               profile.setLastName(lastName.length() > 0 ? lastName : null);
+                               profile.setBirthDay(birthDay).setBirthMonth(birthMonth).setBirthYear(birthYear);
+                               for (Field field : fields) {
+                                       String value = request.getHttpRequest().getPartAsStringFailsafe("field-" + field.getId(), 400);
+                                       field.setValue(value);
+                               }
                                currentSone.setProfile(profile);
+                               webInterface.getCore().saveSone(currentSone);
+                               throw new RedirectException("editProfile.html");
+                       } else if (request.getHttpRequest().getPartAsStringFailsafe("add-field", 4).equals("true")) {
+                               String fieldName = request.getHttpRequest().getPartAsStringFailsafe("field-name", 256).trim();
+                               try {
+                                       profile.addField(fieldName);
+                                       currentSone.setProfile(profile);
+                                       fields = profile.getFields();
+                                       webInterface.getCore().saveSone(currentSone);
+                                       throw new RedirectException("editProfile.html#profile-fields");
+                               } catch (IllegalArgumentException iae1) {
+                                       dataProvider.set("fieldName", fieldName);
+                                       dataProvider.set("duplicateFieldName", true);
+                               }
+                       } else {
+                               String id = getFieldId(request, "delete-field-");
+                               if (id != null) {
+                                       throw new RedirectException("deleteProfileField.html?field=" + id);
+                               }
+                               id = getFieldId(request, "move-up-field-");
+                               if (id != null) {
+                                       Field field = profile.getFieldById(id);
+                                       if (field == null) {
+                                               throw new RedirectException("invalid.html");
+                                       }
+                                       profile.moveFieldUp(field);
+                                       currentSone.setProfile(profile);
+                                       throw new RedirectException("editProfile.html#profile-fields");
+                               }
+                               id = getFieldId(request, "move-down-field-");
+                               if (id != null) {
+                                       Field field = profile.getFieldById(id);
+                                       if (field == null) {
+                                               throw new RedirectException("invalid.html");
+                                       }
+                                       profile.moveFieldDown(field);
+                                       currentSone.setProfile(profile);
+                                       throw new RedirectException("editProfile.html#profile-fields");
+                               }
+                               id = getFieldId(request, "edit-field-");
+                               if (id != null) {
+                                       throw new RedirectException("editProfileField.html?field=" + id);
+                               }
                        }
-                       throw new RedirectException("index.html");
                }
                dataProvider.set("firstName", firstName);
                dataProvider.set("middleName", middleName);
@@ -85,6 +135,30 @@ public class EditProfilePage extends SoneTemplatePage {
                dataProvider.set("birthDay", birthDay);
                dataProvider.set("birthMonth", birthMonth);
                dataProvider.set("birthYear", birthYear);
+               dataProvider.set("fields", fields);
        }
 
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Searches for a part whose names starts with the given {@code String} and
+        * extracts the ID from the located name.
+        *
+        * @param request
+        *            The request to get the parts from
+        * @param partNameStart
+        *            The start of the name of the requested part
+        * @return The parsed ID, or {@code null} if there was no part matching the
+        *         given string
+        */
+       private String getFieldId(Request request, String partNameStart) {
+               for (String partName : request.getHttpRequest().getParts()) {
+                       if (partName.startsWith(partNameStart)) {
+                               return partName.substring(partNameStart.length());
+                       }
+               }
+               return null;
+       }
 }
index 6085ff8..250bff0 100644 (file)
@@ -46,6 +46,7 @@ import net.pterodactylus.sone.template.CollectionAccessor;
 import net.pterodactylus.sone.template.CssClassNameFilter;
 import net.pterodactylus.sone.template.GetPagePlugin;
 import net.pterodactylus.sone.template.IdentityAccessor;
+import net.pterodactylus.sone.template.JavascriptFilter;
 import net.pterodactylus.sone.template.NotificationManagerAccessor;
 import net.pterodactylus.sone.template.PostAccessor;
 import net.pterodactylus.sone.template.ReplyAccessor;
@@ -55,8 +56,10 @@ import net.pterodactylus.sone.template.SubstringFilter;
 import net.pterodactylus.sone.web.ajax.CreatePostAjaxPage;
 import net.pterodactylus.sone.web.ajax.CreateReplyAjaxPage;
 import net.pterodactylus.sone.web.ajax.DeletePostAjaxPage;
+import net.pterodactylus.sone.web.ajax.DeleteProfileFieldAjaxPage;
 import net.pterodactylus.sone.web.ajax.DeleteReplyAjaxPage;
 import net.pterodactylus.sone.web.ajax.DismissNotificationAjaxPage;
+import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
 import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetPostAjaxPage;
@@ -67,6 +70,7 @@ import net.pterodactylus.sone.web.ajax.LikeAjaxPage;
 import net.pterodactylus.sone.web.ajax.LockSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.MarkPostAsKnownPage;
 import net.pterodactylus.sone.web.ajax.MarkReplyAsKnownPage;
+import net.pterodactylus.sone.web.ajax.MoveProfileFieldAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnfollowSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnlikeAjaxPage;
 import net.pterodactylus.sone.web.ajax.UnlockSoneAjaxPage;
@@ -170,6 +174,7 @@ public class WebInterface implements CoreListener {
                templateFactory.addFilter("change", new RequestChangeFilter());
                templateFactory.addFilter("match", new MatchFilter());
                templateFactory.addFilter("css", new CssClassNameFilter());
+               templateFactory.addFilter("js", new JavascriptFilter());
                templateFactory.addPlugin("getpage", new GetPagePlugin());
                templateFactory.addPlugin("paginate", new PaginationPlugin());
                templateFactory.setTemplateProvider(new ClassPathTemplateProvider(templateFactory));
@@ -473,6 +478,8 @@ public class WebInterface implements CoreListener {
                Template createPostTemplate = templateFactory.createTemplate(createReader("/templates/createPost.html"));
                Template createReplyTemplate = templateFactory.createTemplate(createReader("/templates/createReply.html"));
                Template editProfileTemplate = templateFactory.createTemplate(createReader("/templates/editProfile.html"));
+               Template editProfileFieldTemplate = templateFactory.createTemplate(createReader("/templates/editProfileField.html"));
+               Template deleteProfileFieldTemplate = templateFactory.createTemplate(createReader("/templates/deleteProfileField.html"));
                Template viewSoneTemplate = templateFactory.createTemplate(createReader("/templates/viewSone.html"));
                Template viewPostTemplate = templateFactory.createTemplate(createReader("/templates/viewPost.html"));
                Template likePostTemplate = templateFactory.createTemplate(createReader("/templates/like.html"));
@@ -489,6 +496,7 @@ public class WebInterface implements CoreListener {
                Template logoutTemplate = templateFactory.createTemplate(createReader("/templates/logout.html"));
                Template optionsTemplate = templateFactory.createTemplate(createReader("/templates/options.html"));
                Template aboutTemplate = templateFactory.createTemplate(createReader("/templates/about.html"));
+               Template invalidTemplate = templateFactory.createTemplate(createReader("/templates/invalid.html"));
                Template postTemplate = templateFactory.createTemplate(createReader("/templates/include/viewPost.html"));
                Template replyTemplate = templateFactory.createTemplate(createReader("/templates/include/viewReply.html"));
 
@@ -497,6 +505,8 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateSonePage(createSoneTemplate, this), "CreateSone"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new KnownSonesPage(knownSonesTemplate, this), "KnownSones"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new EditProfilePage(editProfileTemplate, this), "EditProfile"));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditProfileFieldPage(editProfileFieldTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteProfileFieldPage(deleteProfileFieldTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreatePostPage(createPostTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateReplyPage(createReplyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new ViewSonePage(viewSoneTemplate, this)));
@@ -516,6 +526,7 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new AboutPage(aboutTemplate, this, SonePlugin.VERSION), "About"));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("noPermission.html", noPermissionTemplate, "Page.NoPermission.Title", this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DismissNotificationPage(dismissNotificationTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new SoneTemplatePage("invalid.html", invalidTemplate, "Page.Invalid.Title", this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("css/", "/static/css/", "text/css")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("javascript/", "/static/javascript/", "text/javascript")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage("images/", "/static/images/", "image/png")));
@@ -537,6 +548,9 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new LikeAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlikeAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetLikesAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditProfileFieldAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteProfileFieldAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new MoveProfileFieldAjaxPage(this)));
 
                ToadletContainer toadletContainer = sonePlugin.pluginRespirator().getToadletContainer();
                toadletContainer.getPageMaker().addNavigationCategory("/Sone/index.html", "Navigation.Menu.Name", "Navigation.Menu.Tooltip", sonePlugin);
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/DeleteProfileFieldAjaxPage.java
new file mode 100644 (file)
index 0000000..383d2d4
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * Sone - DeleteProfileFieldAjaxPage.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user delete a profile field.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DeleteProfileFieldAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new “delete profile field” AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public DeleteProfileFieldAjaxPage(WebInterface webInterface) {
+               super("deleteProfileField.ajax", webInterface);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               String fieldId = request.getHttpRequest().getParam("field");
+               Sone currentSone = getCurrentSone(request.getToadletContext());
+               Profile profile = currentSone.getProfile();
+               Field field = profile.getFieldById(fieldId);
+               if (field == null) {
+                       return createErrorJsonObject("invalid-field-id");
+               }
+               profile.removeField(field);
+               currentSone.setProfile(profile);
+               webInterface.getCore().saveSone(currentSone);
+               return createSuccessJsonObject().put("field", new JsonObject().put("id", field.getId()));
+       }
+
+}
index 283e924..08e3ee5 100644 (file)
@@ -55,4 +55,12 @@ public class DismissNotificationAjaxPage extends JsonPage {
                return createSuccessJsonObject();
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
 }
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/EditProfileFieldAjaxPage.java
new file mode 100644 (file)
index 0000000..0036545
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * Sone - EditProfileFieldAjaxPage.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user rename a profile field.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditProfileFieldAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new “edit profile field” AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public EditProfileFieldAjaxPage(WebInterface webInterface) {
+               super("editProfileField.ajax", webInterface);
+       }
+
+       //
+       // JSONPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               String fieldId = request.getHttpRequest().getParam("field");
+               Sone currentSone = getCurrentSone(request.getToadletContext());
+               Profile profile = currentSone.getProfile();
+               Field field = profile.getFieldById(fieldId);
+               if (field == null) {
+                       return createErrorJsonObject("invalid-field-id");
+               }
+               String name = request.getHttpRequest().getParam("name", "").trim();
+               if (name.length() == 0) {
+                       return createErrorJsonObject("invalid-parameter-name");
+               }
+               Field existingField = profile.getFieldByName(name);
+               if ((existingField != null) && !existingField.equals(field)) {
+                       return createErrorJsonObject("duplicate-field-name");
+               }
+               field.setName(name);
+               currentSone.setProfile(profile);
+               return createSuccessJsonObject();
+       }
+
+}
index a358fb1..659c8d2 100644 (file)
@@ -120,6 +120,14 @@ public class GetStatusAjaxPage extends JsonPage {
                return false;
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
        //
        // PRIVATE METHODS
        //
index 36327ca..725b13a 100644 (file)
@@ -59,4 +59,12 @@ public class GetTranslationPage extends JsonPage {
                return false;
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
 }
index 605afaf..8d48bce 100644 (file)
@@ -137,6 +137,16 @@ public abstract class JsonPage implements Page {
                return true;
        }
 
+       /**
+        * Returns whether this page requires the user to be logged in.
+        *
+        * @return {@code true} if the user needs to be logged in to use this page,
+        *         {@code false} otherwise
+        */
+       protected boolean requiresLogin() {
+               return true;
+       }
+
        //
        // PROTECTED METHODS
        //
@@ -184,6 +194,11 @@ public abstract class JsonPage implements Page {
                                return new Response(401, "Not authorized", "application/json", JsonUtils.format(new JsonObject().put("success", false).put("error", "auth-required")));
                        }
                }
+               if (requiresLogin()) {
+                       if (getCurrentSone(request.getToadletContext(), false) == null) {
+                               return new Response(401, "Not authorized", "application/json", JsonUtils.format(createErrorJsonObject("auth-required")));
+                       }
+               }
                JsonObject jsonObject = createJsonObject(request);
                return new Response(200, "OK", "application/json", JsonUtils.format(jsonObject));
        }
index 6c4ece0..bca9a35 100644 (file)
@@ -53,4 +53,12 @@ public class LockSoneAjaxPage extends JsonPage {
                return createSuccessJsonObject();
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
 }
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/MoveProfileFieldAjaxPage.java
new file mode 100644 (file)
index 0000000..780e4d1
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Sone - MoveProfileFieldAjaxPage.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import net.pterodactylus.sone.data.Profile;
+import net.pterodactylus.sone.data.Profile.Field;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * AJAX page that lets the user move a profile field up or down.
+ *
+ * @see Profile#moveFieldUp(Field)
+ * @see Profile#moveFieldDown(Field)
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class MoveProfileFieldAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new “move profile field” AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public MoveProfileFieldAjaxPage(WebInterface webInterface) {
+               super("moveProfileField.ajax", webInterface);
+       }
+
+       //
+       // JSONPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(Request request) {
+               Sone currentSone = getCurrentSone(request.getToadletContext());
+               Profile profile = currentSone.getProfile();
+               String fieldId = request.getHttpRequest().getParam("field");
+               Field field = profile.getFieldById(fieldId);
+               if (field == null) {
+                       return createErrorJsonObject("invalid-field-id");
+               }
+               String direction = request.getHttpRequest().getParam("direction");
+               try {
+                       if ("up".equals(direction)) {
+                               profile.moveFieldUp(field);
+                       } else if ("down".equals(direction)) {
+                               profile.moveFieldDown(field);
+                       } else {
+                               return createErrorJsonObject("invalid-direction");
+                       }
+               } catch (IllegalArgumentException iae1) {
+                       return createErrorJsonObject("not-possible");
+               }
+               currentSone.setProfile(profile);
+               webInterface.getCore().saveSone(currentSone);
+               return createSuccessJsonObject();
+       }
+
+}
index 02682b2..e1c408c 100644 (file)
@@ -53,4 +53,12 @@ public class UnlockSoneAjaxPage extends JsonPage {
                return createSuccessJsonObject();
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected boolean requiresLogin() {
+               return false;
+       }
+
 }
index 740cf45..ccedff0 100644 (file)
@@ -70,8 +70,32 @@ Page.EditProfile.Birthday.Title=Birthday
 Page.EditProfile.Birthday.Label.Day=Day:
 Page.EditProfile.Birthday.Label.Month=Month:
 Page.EditProfile.Birthday.Label.Year=Year:
-Page.EditProfile.Page.Status.Changed=Your changes have been saved and will be inserted shortly.
+Page.EditProfile.Fields.Title=Custom Fields
+Page.EditProfile.Fields.Description=Here you can enter custom fields into your profile. These fields can contain anything you want and be as terse or as verbose as you wish. Just remember that when it comes to anonymity, sometimes less is more.
+Page.EditProfile.Fields.Button.Edit=edit
+Page.EditProfile.Fields.Button.MoveUp=move up
+Page.EditProfile.Fields.Button.MoveDown=move down
+Page.EditProfile.Fields.Button.Delete=delete
+Page.EditProfile.Fields.Button.ReallyDelete=really delete
+Page.EditProfile.Fields.AddField.Title=Add Field
+Page.EditProfile.Fields.AddField.Label.Name=Name:
+Page.EditProfile.Fields.AddField.Button.AddField=Add Field
 Page.EditProfile.Button.Save=Save Profile
+Page.EditProfile.Error.DuplicateFieldName=The field name “{fieldName}” does already exist.
+
+Page.EditProfileField.Title=Edit Profile Field - Sone
+Page.EditProfileField.Page.Title=Edit Profile Field
+Page.EditProfileField.Text=Enter a new name for this profile field.
+Page.EditProfileField.Error.DuplicateFieldName=The field name you entered does already exist.
+Page.EditProfileField.Button.Save=Change
+Page.EditProfileField.Button.Reset=Revert to old name
+Page.EditProfileField.Button.Cancel=Do not change name
+
+Page.DeleteProfileField.Title=Delete Profile Field - Sone
+Page.DeleteProfileField.Page.Title=Delete Profile Field
+Page.DeleteProfileField.Text=Do you really want to delete this profile field?
+Page.DeleteProfileField.Button.Yes=Yes, delete
+Page.DeleteProfileField.Button.No=No, do not delete
 
 Page.CreatePost.Title=Create Post - Sone
 Page.CreatePost.Page.Title=Create Post
@@ -92,6 +116,8 @@ Page.ViewSone.UnknownSone.Description=This Sone has not yet been retrieved. Plea
 Page.ViewSone.WriteAMessage=You can write a message to this Sone here. Please note that everybody will be able to read this message!
 Page.ViewSone.PostList.Title=Posts by {sone}
 Page.ViewSone.PostList.Text.NoPostYet=This Sone has not yet posted anything.
+Page.ViewSone.Profile.Title=Profile
+Page.ViewSone.Profile.Label.Name=Name
 
 Page.ViewPost.Title=View Post - Sone
 Page.ViewPost.Page.Title=View Post by {sone}
@@ -132,6 +158,10 @@ Page.WotPluginMissing.Text.LoadPlugin=Please load the Web of Trust plugin in the
 
 Page.Logout.Title=Logout - Sone
 
+Page.Invalid.Title=Invalid Action Performed
+Page.Invalid.Page.Title=Invalid Action Performed
+Page.Invalid.Text=An invalid action was performed, or the action was valid but the parameters were not. Please go back to the {link}index page{/link} and try again. If the error persists you have probably found a bug.
+
 View.CreateSone.Text.WotIdentityRequired=To create a Sone you need an identity from the {link}Web of Trust plugin{/link}.
 View.CreateSone.Select.Default=Select an identity
 View.CreateSone.Text.NoIdentities=You do not have any Web of Trust identities. Please head over to the {link}Web of Trust plugin{/link} and create an identity.
@@ -168,6 +198,7 @@ WebInterface.DefaultText.LastName=Last name
 WebInterface.DefaultText.BirthDay=Day
 WebInterface.DefaultText.BirthMonth=Month
 WebInterface.DefaultText.BirthYear=Year
+WebInterface.DefaultText.FieldName=Field name
 WebInterface.DefaultText.Option.InsertionDelay=Time to wait after a Sone is modified before insert (in seconds)
 WebInterface.Confirmation.DeletePostButton=Yes, delete!
 WebInterface.Confirmation.DeleteReplyButton=Yes, delete!
index 4f0c49e..f444cab 100644 (file)
@@ -16,6 +16,10 @@ input[type=text], textarea {
        outline: none;
 }
 
+input[type=text].short {
+       width: 25em;
+}
+
 textarea {
        height: 4em;
 }
@@ -438,8 +442,40 @@ textarea {
        display: inline;
 }
 
-#sone #create-sone {
+#sone .profile-field, #sone #edit-profile button[type=submit], #sone #delete-profile-field {
+       margin-top: 1em;
+}
 
+#sone .profile-field .name {
+       display: inline;
+       font-weight: bold;
+}
+
+#sone .profile-field .name.hidden {
+       display: none;
+}
+
+#sone .profile-field button.confirm {
+       font-weight: bold;
+       color: #080;
+}
+
+#sone .profile-field button.cancel {
+       font-weight: bold;
+       color: red;
+}
+
+#sone .profile-field .value {
+       margin-left: 2em;
+}
+
+#sone #edit-profile .profile-field .value {
+       margin-left: inherit;
+}
+
+#sone .profile-field .edit-field-name, #sone .profile-field .move-up-field, #sone .profile-field .move-down-field, #sone .profile-field .delete-field-name {
+       float: right;
+       margin-top: -1ex;
 }
 
 #sone #tail {
@@ -525,6 +561,6 @@ textarea {
 }
 
 #sone .confirm {
-       font-weight: bold !important;
-       color: red !important;
+       font-weight: bold;
+       color: red;
 }
index 41a2054..2d6c8ec 100644 (file)
@@ -866,6 +866,80 @@ function showNotificationDetails(notificationId) {
        $("#sone .notification#" + notificationId + " .short-text").hide();
 }
 
+/**
+ * Deletes the field with the given ID from the profile.
+ *
+ * @param fieldId
+ *            The ID of the field to delete
+ */
+function deleteProfileField(fieldId) {
+       $.getJSON("deleteProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId}, function(data, textStatus) {
+               if (data && data.success) {
+                       $("#sone .profile-field#" + data.field.id).slideUp();
+               }
+       });
+}
+
+/**
+ * Renames a profile field.
+ *
+ * @param fieldId
+ *            The ID of the field to rename
+ * @param newName
+ *            The new name of the field
+ * @param successFunction
+ *            Called when the renaming was successful
+ */
+function editProfileField(fieldId, newName, successFunction) {
+       $.getJSON("editProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "name": newName}, function(data, textStatus) {
+               if (data && data.success) {
+                       successFunction();
+               }
+       });
+}
+
+/**
+ * Moves the profile field with the given ID one slot in the given direction.
+ *
+ * @param fieldId
+ *            The ID of the field to move
+ * @param direction
+ *            The direction to move in (“up” or “down”)
+ * @param successFunction
+ *            Function to call on success
+ */
+function moveProfileField(fieldId, direction, successFunction) {
+       $.getJSON("moveProfileField.ajax", {"formPassword": getFormPassword(), "field": fieldId, "direction": direction}, function(data, textStatus) {
+               if (data && data.success) {
+                       successFunction();
+               }
+       });
+}
+
+/**
+ * Moves the profile field with the given ID up one slot.
+ *
+ * @param fieldId
+ *            The ID of the field to move
+ * @param successFunction
+ *            Function to call on success
+ */
+function moveProfileFieldUp(fieldId, successFunction) {
+       moveProfileField(fieldId, "up", successFunction);
+}
+
+/**
+ * Moves the profile field with the given ID down one slot.
+ *
+ * @param fieldId
+ *            The ID of the field to move
+ * @param successFunction
+ *            Function to call on success
+ */
+function moveProfileFieldDown(fieldId, successFunction) {
+       moveProfileField(fieldId, "down", successFunction);
+}
+
 //
 // EVERYTHING BELOW HERE IS EXECUTED AFTER LOADING THE PAGE
 //
diff --git a/src/main/resources/templates/deleteProfileField.html b/src/main/resources/templates/deleteProfileField.html
new file mode 100644 (file)
index 0000000..006e1ff
--- /dev/null
@@ -0,0 +1,19 @@
+<%include include/head.html>
+
+       <h1><%= Page.DeleteProfileField.Page.Title|l10n|html></h1>
+
+       <p><%= Page.DeleteProfileField.Text|l10n|html></p>
+
+       <div class="profile-field">
+               <div class="name"><% field.name|html></div>
+               <div class="value"><% field.value|html></div>
+       </div>
+
+       <form id="delete-profile-field" method="post">
+               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+               <input type="hidden" name="field" value="<% field.id|html>" />
+               <button type="submit" name="confirm" value="true"><%= Page.DeleteProfileField.Button.Yes|l10n|html></button>
+               <button type="submit" name="cancel" value="true"><%= Page.DeleteProfileField.Button.No|l10n|html></button>
+       </form>
+
+<%include include/tail.html>
index d1d5943..8a1b7d7 100644 (file)
@@ -1,7 +1,14 @@
 <%include include/head.html>
 
        <script language="javascript">
-               $(document).ready(function() {
+               function recheckMoveButtons() {
+                       $("#sone .profile-field").each(function() {
+                               $(".move-up-field", this).toggleClass("hidden", $(this).prev(".profile-field").length == 0);
+                               $(".move-down-field", this).toggleClass("hidden", $(this).next(".profile-field").length == 0);
+                       });
+               }
+
+               $(function() {
                        getTranslation("WebInterface.DefaultText.FirstName", function(firstNameDefaultText) {
                                registerInputTextareaSwap("#sone #edit-profile input[name=first-name]", firstNameDefaultText, "first-name", true, true);
                        });
                        getTranslation("WebInterface.DefaultText.BirthYear", function(birthYearDefaultText) {
                                registerInputTextareaSwap("#sone #edit-profile input[name=birth-year]", birthYearDefaultText, "birth-year", true, true);
                        });
+                       getTranslation("WebInterface.DefaultText.FieldName", function(fieldNameDefaultText) {
+                               registerInputTextareaSwap("#sone #add-profile-field input[name=field-name]", fieldNameDefaultText, "field-name", true, true);
+                       });
+
+                       <%foreach fields field>
+                               registerInputTextareaSwap("#sone #edit-profile input[name=field-<% loop.count>]", <% field.key|js>, "field-<% loop.count>", true, true);
+                       <%/foreach>
 
                        /* hide all the labels. */
-                       $("#sone #edit-profile label").hide();
+                       $("#sone #edit-profile label, #sone #add-profile-field label").hide();
+
+                       /* ajaxify the delete buttons. */
+                       getTranslation("Page.EditProfile.Fields.Button.ReallyDelete", function(reallyDeleteText) {
+                               $("#sone #edit-profile .delete-field-name button").each(function() {
+                                       confirmButton = $(this).clone().addClass("hidden").addClass("confirm").text(reallyDeleteText).insertAfter(this);
+                                       (function(deleteButton, confirmButton) {
+                                               deleteButton.click(function() {
+                                                       deleteButton.fadeOut("slow", function() {
+                                                               confirmButton.fadeIn("slow");
+                                                               $(document).one("click", function() {
+                                                                       if (this != confirmButton.get(0)) {
+                                                                               confirmButton.fadeOut("slow", function() {
+                                                                                       deleteButton.fadeIn("slow");
+                                                                               });
+                                                                       }
+                                                                       return false;
+                                                               });
+                                                       });
+                                                       return false;
+                                               });
+                                               confirmButton.click(function() {
+                                                       confirmButton.fadeOut("slow");
+                                                       buttonName = confirmButton.attr("name");
+                                                       fieldId = buttonName.substring("delete-field-".length);
+                                                       deleteProfileField(fieldId);
+                                                       recheckMoveButtons();
+                                                       return false;
+                                               });
+                                       })($(this), confirmButton);
+                               });
+                       });
+
+                       /* ajaxify the edit button. */
+                       $("#sone #edit-profile .edit-field-name button").each(function() {
+                               profileField = $(this).parents(".profile-field");
+                               fieldNameElement = profileField.find(".name");
+                               inputField = $("input[type=text].short", profileField);
+                               confirmButton = $("button.confirm", profileField);
+                               cancelButton = $("button.cancel", profileField);
+                               (function(editButton, inputField, confirmButton, cancelButton, fieldNameElement) {
+                                       cleanUp = function(editButton, inputField, confirmButton, cancelButton, fieldNameElement) {
+                                               editButton.removeAttr("disabled");
+                                               inputField.addClass("hidden");
+                                               confirmButton.addClass("hidden");
+                                               cancelButton.addClass("hidden");
+                                               fieldNameElement.removeClass("hidden");
+                                       };
+                                       confirmButton.click(function() {
+                                               inputField.attr("disabled", "disabled");
+                                               confirmButton.attr("disabled", "disabled");
+                                               cancelButton.attr("disabled", "disabled");
+                                               editProfileField(confirmButton.parents(".profile-field").attr("id"), inputField.val(), function() {
+                                                       fieldNameElement.text(inputField.val());
+                                                       cleanUp(editButton, inputField, confirmButton, cancelButton, fieldNameElement);
+                                               });
+                                               return false;
+                                       });
+                                       cancelButton.click(function() {
+                                               cleanUp(editButton, inputField, confirmButton, cancelButton, fieldNameElement);
+                                               return false;
+                                       });
+                                       inputField.keypress(function(event) {
+                                               if (event.which == 13) {
+                                                       confirmButton.click();
+                                                       return false;
+                                               } else if (event.which == 27) {
+                                                       cancelButton.click();
+                                                       return false;
+                                               }
+                                       });
+                                       editButton.click(function() {
+                                               editButton.attr("disabled", "disabled");
+                                               fieldNameElement.addClass("hidden");
+                                               inputField.removeAttr("disabled").val(fieldNameElement.text()).removeClass("hidden").focus().select();
+                                               confirmButton.removeAttr("disabled").removeClass("hidden");
+                                               cancelButton.removeAttr("disabled").removeClass("hidden");
+                                               return false;
+                                       });
+                               })($(this), inputField, confirmButton, cancelButton, fieldNameElement);
+                       });
+
+                       /* ajaxify “move up” and “move down” buttons. */
+                       $("#sone .profile-field .move-down-field button").click(function() {
+                               profileField = $(this).parents(".profile-field");
+                               moveProfileFieldDown(profileField.attr("id"), function() {
+                                       next = profileField.next();
+                                       current = profileField.insertAfter(next);
+                                       recheckMoveButtons();
+                               });
+                               return false;
+                       });
+                       $("#sone .profile-field .move-up-field button").click(function() {
+                               profileField = $(this).parents(".profile-field");
+                               moveProfileFieldUp(profileField.attr("id"), function() {
+                                       previous = profileField.prev();
+                                       current = profileField.insertBefore(previous);
+                                       recheckMoveButtons();
+                               });
+                               return false;
+                       });
                });
        </script>
 
        <p><%= Page.EditProfile.Page.Description|l10n|html></p>
        <p><%= Page.EditProfile.Page.Hint.Optionality|l10n|html></p>
 
-       <%if changed>
-               <p><%= Page.EditProfile.Page.Status.Changed|l10n|html></p>
-       <%/if>
-
        <form id="edit-profile" method="post">
                <input type="hidden" name="formPassword" value="<% formPassword|html>" />
 
                </div>
 
                <div>
-                       <button type="submit"><%= Page.EditProfile.Button.Save|l10n|html></button>
+                       <button type="submit" name="save-profile" value="true"><%= Page.EditProfile.Button.Save|l10n|html></button>
+               </div>
+
+               <h1><%= Page.EditProfile.Fields.Title|l10n|html></h1>
+
+               <p><%= Page.EditProfile.Fields.Description|l10n|html></p>
+
+               <%foreach fields field fieldLoop>
+                       <div class="profile-field" id="<% field.id|html>">
+                               <div class="name"><% field.name|html></div>
+                               <input class="short hidden" type="text"><button class="confirm hidden" type="button">✔</button><button class="cancel hidden" type="button">✘</button>
+                               <div class="edit-field-name"><button type="submit" name="edit-field-<% field.id|html>" value="true"><%= Page.EditProfile.Fields.Button.Edit|l10n|html></button></div>
+                               <div class="delete-field-name"><button type="submit" name="delete-field-<% field.id|html>" value="true"><%= Page.EditProfile.Fields.Button.Delete|l10n|html></button></div>
+                               <div class="<%if fieldLoop.last>hidden <%/if>move-down-field"><button type="submit" name="move-down-field-<% field.id|html>" value="true"><%= Page.EditProfile.Fields.Button.MoveDown|l10n|html></button></div>
+                               <div class="<%if fieldLoop.first>hidden <%/if>move-up-field"><button type="submit" name="move-up-field-<% field.id|html>" value="true"><%= Page.EditProfile.Fields.Button.MoveUp|l10n|html></button></div>
+                               <div class="value"><input type="text" name="field-<% field.id|html>" value="<% field.value|html>" /></div>
+                       </div>
+
+                       <%if fieldLoop.last>
+                               <div>
+                                       <button type="submit" name="save-profile" value="true"><%= Page.EditProfile.Button.Save|l10n|html></button>
+                               </div>
+                       <%/if>
+               <%/foreach>
+
+       </form>
+
+       <form id="add-profile-field" method="post">
+               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+
+               <a name="profile-fields"></a>
+               <h2><%= Page.EditProfile.Fields.AddField.Title|l10n|html></h2>
+
+               <%if duplicateFieldName>
+                       <p><%= Page.EditProfile.Error.DuplicateFieldName|l10n|replace needle="{fieldName}" replacementKey="fieldName"|html></p>
+               <%/if>
+
+               <div id="new-field">
+                       <label for="new-field"><%= Page.EditProfile.Fields.AddField.Label.Name|l10n|html></label>
+                       <input type="text" name="field-name" value="" />
+                       <button type="submit" name="add-field" value="true"><%= Page.EditProfile.Fields.AddField.Button.AddField|l10n|html></button>
                </div>
 
        </form>
diff --git a/src/main/resources/templates/editProfileField.html b/src/main/resources/templates/editProfileField.html
new file mode 100644 (file)
index 0000000..6030f71
--- /dev/null
@@ -0,0 +1,24 @@
+<%include include/head.html>
+
+       <h1><%= Page.EditProfileField.Page.Title|l10n|html></h1>
+
+       <p><%= Page.EditProfileField.Text|l10n|html></p>
+
+       <%if duplicateFieldName>
+               <p><%= Page.EditProfileField.Error.DuplicateFieldName|l10n|html></p>
+       <%/if>
+
+       <form method="post">
+               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+               <input type="hidden" name="field" value="<% field.id|html>" />
+               <div>
+                       <input type="text" name="name" value="<% field.name|html>" />
+                       <button type="submit" name="save" value="true"><%= Page.EditProfileField.Button.Save|l10n|html></button>
+               </div>
+               <p>
+                       <button type="reset"><%= Page.EditProfileField.Button.Reset|l10n|html></button>
+                       <button type="submit" name="cancel" value="true"><%= Page.EditProfileField.Button.Cancel|l10n|html></button>
+               </p>
+       </form>
+
+<%include include/tail.html>
index 7d3c8aa..9ba037f 100644 (file)
                <birth-day><% currentSone.profile.birthDay|xml></birth-day>
                <birth-month><% currentSone.profile.birthMonth|xml></birth-month>
                <birth-year><% currentSone.profile.birthYear|xml></birth-year>
+               <fields>
+                       <%foreach currentSone.profile.fields field>
+                       <field>
+                               <field-name><% field.key|xml></field-name>
+                               <field-value><% field.value|xml></field-value>
+                       </field>
+                       <%/foreach>
+               </fields>
        </profile>
 
        <posts>
diff --git a/src/main/resources/templates/invalid.html b/src/main/resources/templates/invalid.html
new file mode 100644 (file)
index 0000000..d838134
--- /dev/null
@@ -0,0 +1,7 @@
+<%include include/head.html>
+
+       <h1><%= Page.Invalid.Page.Title|l10n|html></h1>
+
+       <p><%= Page.Invalid.Text|l10n|html|replace needle="{link}" replacement='<a href="index.html">'|replace needle="{/link}" replacement='</a>'></p>
+
+<%include include/tail.html>
index 884fa0f..c6dadf7 100644 (file)
 
                <%if ! sone.current>
                        <%include include/viewSone.html>
+               <%/if>
+
+               <h1><%= Page.ViewSone.Profile.Title|l10n|html></h1>
 
+                       <div class="profile-field">
+                               <div class="name"><%= Page.ViewSone.Profile.Label.Name|l10n|html></div>
+                               <div class="value"><% sone.niceName|html></div>
+                       </div>
+
+                       <%foreach sone.profile.fields field>
+                               <div class="profile-field">
+                                       <div class="name"><% field.name|html></div>
+                                       <div class="value"><% field.value|html></div>
+                               </div>
+                       <%/foreach>
+
+               <%if ! sone.current>
                        <p><%= Page.ViewSone.WriteAMessage|l10n|html></p>
 
                        <form action="createPost.html" id="post-message" method="post">
@@ -31,7 +47,6 @@
                        </form>
                <%/if>
 
-
                <h1><%= Page.ViewSone.PostList.Title|l10n|insert needle="{sone}" key=sone.niceName|html></h1>
 
                <div id="posts">