Commit 3120a70c by Jose Antonio

Merged branch develop into develop

parents 74758708 200f8d09
Showing with 481 additions and 309 deletions
...@@ -62,6 +62,10 @@ android/Pictogram/commonlibrary/build ...@@ -62,6 +62,10 @@ android/Pictogram/commonlibrary/build
android/Pictogram/tablet/build android/Pictogram/tablet/build
android/Pictogram/watch/build android/Pictogram/watch/build
android/Pictogram/build android/Pictogram/build
android/Pictogram/supervisor_tablet/supervisor_tablet.iml
android/Pictogram/tabletlibrary/tabletlibrary.iml
android/Pictogram/yotta_tablet/yotta_tablet.iml
android/Pictogram/watch/watch.iml
# OS generated files # # OS generated files #
###################### ######################
......
...@@ -142,7 +142,7 @@ public class NetService implements Runnable, RestapiWrapper.iSilentLogin { ...@@ -142,7 +142,7 @@ public class NetService implements Runnable, RestapiWrapper.iSilentLogin {
public void restart_app(boolean direct_login) { public void restart_app(boolean direct_login) {
for (iNetServiceStatus listener: listeners) for (iNetServiceStatus listener: listeners)
if (listener instanceof iNetServiceDevice) ((iNetServiceDevice)listener).restart_app(direct_login); if (listener instanceof iNetServiceDevice) ((iNetServiceDevice)listener).restart_app(direct_login);
PCBcontext.unset_user(); PCBcontext.unset_user();
} }
public void restart_app(Intent intent, boolean direct_login) { public void restart_app(Intent intent, boolean direct_login) {
for (iNetServiceStatus listener: listeners) for (iNetServiceStatus listener: listeners)
...@@ -155,66 +155,68 @@ public class NetService implements Runnable, RestapiWrapper.iSilentLogin { ...@@ -155,66 +155,68 @@ public class NetService implements Runnable, RestapiWrapper.iSilentLogin {
*/ */
@Override @Override
public void run() { public void run() {
PCBcontext.getRestapiWrapper().ask(ping_session, new RestapiWrapper.iRestapiListener() { try {
@Override PCBcontext.getRestapiWrapper().ask(ping_session, new RestapiWrapper.iRestapiListener() {
public void preExecute() { @Override
public void preExecute() {
}
@Override
public void result(JSONArray result) {
}
@Override
public void result(JSONObject result) {
try {
float version=Float.valueOf(result.getString("version")).floatValue();
if (PCBcontext.getActivityContext()!=null && version> DeviceHelper.getAppVersion(PCBcontext.getActivityContext())) {
Log.e(LOG_TAG,"New version is required! from v"+DeviceHelper.getAppVersion(PCBcontext.getContext())+" to v"+version);
newVersionAlert(PCBcontext.getActivityContext(),version);
}
} catch (JSONException e) {
Log.e(LOG_TAG,"PING JSON ERROR: "+result+" "+e.getMessage());
} }
if (!updated) {
lastRestfullSynchro=new Date().getTime(); @Override
updated = true; public void result(JSONArray result) {
if (PCBcontext.is_user_logged()) //si el usuario aun no hizo login, en realidad no es necesario hacer nada
// Comprobar si hay usuario offline, para hacer login transparente
if (PCBcontext.is_user_offline()){
Log.i(LOG_TAG, "PCB online login from offline login");
login();
} else if (PCBcontext.is_user_online()){
Log.i(LOG_TAG, "PCB reconnect");
PCBcontext.getRoom().connect();
PCBcontext.getVocabulary().synchronize();
PCBcontext.getActionLog().batch();
}
notifyStatus();
} }
else {
//cada restfullSynchroTimming aprox. se fuerza sincronización de vocabulario y configuración de usuario
long now=new Date().getTime();
if (PCBcontext.is_user_logged()) {
if (restfullSynchroTimming>0 && (now - lastRestfullSynchro > restfullSynchroTimming)) {
Log.i(LOG_TAG, "Vocabulary request");
PCBcontext.getVocabulary().synchronize();
synchronizeStudentAttributes();
lastRestfullSynchro = now;
}
}
else lastRestfullSynchro=new Date().getTime();
}
} @Override
public void result(JSONObject result) {
try {
float version = Float.valueOf(result.getString("version")).floatValue();
if (PCBcontext.getActivityContext() != null && version > DeviceHelper.getAppVersion(PCBcontext.getActivityContext())) {
Log.e(LOG_TAG, "New version is required! from v" + DeviceHelper.getAppVersion(PCBcontext.getContext()) + " to v" + version);
newVersionAlert(PCBcontext.getActivityContext(), version);
}
} catch (JSONException e) {
Log.e(LOG_TAG, "PING JSON ERROR: " + result + " " + e.getMessage());
}
if (!updated) {
lastRestfullSynchro = new Date().getTime();
updated = true;
if (PCBcontext.is_user_logged()) //si el usuario aun no hizo login, en realidad no es necesario hacer nada
// Comprobar si hay usuario offline, para hacer login transparente
if (PCBcontext.is_user_offline()) {
Log.i(LOG_TAG, "PCB online login from offline login");
login();
} else if (PCBcontext.is_user_online()) {
Log.i(LOG_TAG, "PCB reconnect");
PCBcontext.getRoom().connect();
PCBcontext.getVocabulary().synchronize();
PCBcontext.getActionLog().batch();
}
} else {
//cada restfullSynchroTimming aprox. se fuerza sincronización de vocabulario y configuración de usuario
long now = new Date().getTime();
if (PCBcontext.is_user_logged()) {
if (restfullSynchroTimming > 0 && (now - lastRestfullSynchro > restfullSynchroTimming)) {
Log.i(LOG_TAG, "Vocabulary request");
PCBcontext.getVocabulary().synchronize();
synchronizeStudentAttributes();
lastRestfullSynchro = now;
}
} else lastRestfullSynchro = new Date().getTime();
}
@Override }
public void error(RestapiWrapper.HTTPException e) {
setOffline(e);
}
});
@Override
public void error(RestapiWrapper.HTTPException e) {
setOffline(e);
}
});
notifyStatus();
}catch(Exception e) {
Log.e(LOG_TAG,"THREAD NOT WORKING BECAUSE:"+e.getMessage());
this.restart_app(true);
}
} }
private void synchronizeStudentAttributes() { private void synchronizeStudentAttributes() {
......
...@@ -39,13 +39,14 @@ public class StudentTalk implements Emitter.Listener { ...@@ -39,13 +39,14 @@ public class StudentTalk implements Emitter.Listener {
Log.i(this.getClass().getName(), "raw Received message " +msg.toString()); Log.i(this.getClass().getName(), "raw Received message " +msg.toString());
int id=msg.getInt("id"); int id=msg.getInt("id");
String username=msg.getString("username"); String username=msg.getString("username");
String name=msg.getString("name");
String surname=msg.getString("surname"); String surname=msg.getString("surname");
String gender=msg.getString("gender"); String gender=msg.getString("gender");
String pic=msg.getString("pic"); String pic=msg.getString("pic");
String lang=msg.getString("lang"); String lang=msg.getString("lang");
String attributes=msg.getString("attributes"); String attributes=msg.getString("attributes");
User user=PCBcontext.getPcbdb().getCurrentUser(); User user=PCBcontext.getPcbdb().getCurrentUser();
User updatedUser=new User(id, username, user.get_pwd_stu(), username, surname, pic, gender, lang, attributes, User updatedUser=new User(id, username, user.get_pwd_stu(), name, surname, pic, gender, lang, attributes,
user.get_id_sup(), user.get_email_sup(), user.get_pwd_sup(), user.get_name_sup(), user.get_surname_sup(), user.get_url_img_sup(), user.get_gender_sup(), user.get_id_sup(), user.get_email_sup(), user.get_pwd_sup(), user.get_name_sup(), user.get_surname_sup(), user.get_url_img_sup(), user.get_gender_sup(),
user.get_lang_sup(), user.get_tts_engine_sup(), user.get_office()); user.get_lang_sup(), user.get_tts_engine_sup(), user.get_office());
Log.i(this.getClass().getName(), "Attributes" +attributes+" listeners:"+listeners.length); Log.i(this.getClass().getName(), "Attributes" +attributes+" listeners:"+listeners.length);
......
...@@ -37,6 +37,11 @@ import com.yottacode.pictogram.tabletlibrary.cropper.util.PaintUtil; ...@@ -37,6 +37,11 @@ import com.yottacode.pictogram.tabletlibrary.cropper.util.PaintUtil;
public class CropImageView extends ImageView { public class CropImageView extends ImageView {
// Private Constants /////////////////////////////////////////////////////////////////////////// // Private Constants ///////////////////////////////////////////////////////////////////////////
private static final int CAMERA_PIC_REQUEST = 1;
private static final int GALLERY_PIC_REQUEST = 2;
private static final int VERTICAL_ORIENTATION = 200;
private static final int HORIZONTAL_ORIENTATION = 300;
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static final String TAG = CropImageView.class.getName(); private static final String TAG = CropImageView.class.getName();
...@@ -248,8 +253,10 @@ public class CropImageView extends ImageView { ...@@ -248,8 +253,10 @@ public class CropImageView extends ImageView {
* Gets the cropped image based on the current crop window. * Gets the cropped image based on the current crop window.
* *
* @return a new Bitmap representing the cropped image * @return a new Bitmap representing the cropped image
* @param origin
* @param orientation
*/ */
public Bitmap getCroppedImage() { public Bitmap getCroppedImage(int origin, int orientation) {
final Drawable drawable = this.getDrawable(); final Drawable drawable = this.getDrawable();
if (drawable == null || !(drawable instanceof BitmapDrawable)) { if (drawable == null || !(drawable instanceof BitmapDrawable)) {
return null; return null;
...@@ -264,8 +271,8 @@ public class CropImageView extends ImageView { ...@@ -264,8 +271,8 @@ public class CropImageView extends ImageView {
final float scaleY = matrixValues[Matrix.MSCALE_Y]; //==1 final float scaleY = matrixValues[Matrix.MSCALE_Y]; //==1
// Extract the translation values. // Extract the translation values.
final float transX = matrixValues[Matrix.MTRANS_X]; final float transX = (origin == CAMERA_PIC_REQUEST && orientation== HORIZONTAL_ORIENTATION ) ? matrixValues[Matrix.MTRANS_Y] : matrixValues[Matrix.MTRANS_X];
final float transY = matrixValues[Matrix.MTRANS_Y]; final float transY = (origin == CAMERA_PIC_REQUEST && orientation== HORIZONTAL_ORIENTATION ) ? matrixValues[Matrix.MTRANS_X] : matrixValues[Matrix.MTRANS_Y];
final Bitmap originalBitmap = ((BitmapDrawable) drawable).getBitmap(); final Bitmap originalBitmap = ((BitmapDrawable) drawable).getBitmap();
/*Log.i("DETALLES","ScaleX: "+scaleX + "- ScaleY: "+scaleY); /*Log.i("DETALLES","ScaleX: "+scaleX + "- ScaleY: "+scaleY);
......
...@@ -25,6 +25,8 @@ public class EditPictoActivity extends Activity { ...@@ -25,6 +25,8 @@ public class EditPictoActivity extends Activity {
public static final int EDIT_PICTO_REQUEST = 2288; public static final int EDIT_PICTO_REQUEST = 2288;
public static final String TRANSCRIPTION = "textPicto"; public static final String TRANSCRIPTION = "textPicto";
public static final String IMAGE_PICTO = "imagePicto"; public static final String IMAGE_PICTO = "imagePicto";
public static final String IMAGE_ORIGIN = "imageOrigin";
public static final String IMAGE_ORIENTATION = "imageOrientation";
// Activity Methods //////////////////////////////////////////////////////////////////////////// // Activity Methods ////////////////////////////////////////////////////////////////////////////
...@@ -47,6 +49,9 @@ public class EditPictoActivity extends Activity { ...@@ -47,6 +49,9 @@ public class EditPictoActivity extends Activity {
cropImageView.setGuidelines(2); cropImageView.setGuidelines(2);
cropImageView.setAspectRatio(1,1); cropImageView.setAspectRatio(1,1);
final int orientation = getIntent().getExtras().getInt(EditPictoActivity.IMAGE_ORIENTATION);
final int origin = getIntent().getExtras().getInt(EditPictoActivity.IMAGE_ORIGIN);
String legendText = getIntent().getExtras().getString(EditPictoActivity.TRANSCRIPTION); String legendText = getIntent().getExtras().getString(EditPictoActivity.TRANSCRIPTION);
if(legendText != null) { if(legendText != null) {
Log.i("DETALLES","Llega el intent al layout recortar, con texto: " + legendText); Log.i("DETALLES","Llega el intent al layout recortar, con texto: " + legendText);
...@@ -71,13 +76,14 @@ public class EditPictoActivity extends Activity { ...@@ -71,13 +76,14 @@ public class EditPictoActivity extends Activity {
okButton.setOnClickListener(new View.OnClickListener() { okButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
final Bitmap croppedImage = cropImageView.getCroppedImage(); final Bitmap croppedImage = cropImageView.getCroppedImage(origin,orientation);
//Escalar y guardarla al server //Escalar y guardarla al server
croppedImage.createScaledBitmap(croppedImage,96,96,true); Bitmap finalImage = null;
finalImage.createScaledBitmap(croppedImage,96,96,true);
Intent intent = getIntent(); Intent intent = getIntent();
intent.putExtra(EditPictoActivity.TRANSCRIPTION,legend.getText().toString()); intent.putExtra(EditPictoActivity.TRANSCRIPTION,legend.getText().toString());
Log.i("DETALLES","sale el intent al layout recortar, con texto: " + legend.getText().toString()); Log.i("DETALLES","sale el intent al layout recortar, con texto: " + legend.getText().toString());
cropImageView.setImageBitmap(croppedImage); cropImageView.setImageBitmap(finalImage);
setResult(RESULT_OK,intent); setResult(RESULT_OK,intent);
finish(); finish();
} }
......
...@@ -57,6 +57,7 @@ import com.yottacode.pictogram.tabletlibrary.net.NetServiceTablet; ...@@ -57,6 +57,7 @@ import com.yottacode.pictogram.tabletlibrary.net.NetServiceTablet;
import com.yottacode.pictogram.tools.Img; import com.yottacode.pictogram.tools.Img;
import com.yottacode.pictogram.tools.PCBcontext; import com.yottacode.pictogram.tools.PCBcontext;
import com.yottacode.pictogram.tts.TTSHelper; import com.yottacode.pictogram.tts.TTSHelper;
import com.yottacode.tools.GUITools;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
...@@ -76,6 +77,9 @@ public class PictogramActivity extends Activity implements VocabularyTalk.iVocab ...@@ -76,6 +77,9 @@ public class PictogramActivity extends Activity implements VocabularyTalk.iVocab
private static final int CAMERA_PIC_REQUEST = 1; private static final int CAMERA_PIC_REQUEST = 1;
private static final int GALLERY_PIC_REQUEST = 2; private static final int GALLERY_PIC_REQUEST = 2;
private static final int VERTICAL_ORIENTATION = 200;
private static final int HORIZONTAL_ORIENTATION = 300;
private static final int MAX_WIDTH = 700; private static final int MAX_WIDTH = 700;
private static final int MAX_HEIGHT = 350; private static final int MAX_HEIGHT = 350;
...@@ -1052,16 +1056,22 @@ protected void showOnlyTape(boolean onlyTape) { ...@@ -1052,16 +1056,22 @@ protected void showOnlyTape(boolean onlyTape) {
if (!student_view || !PCBcontext.getPcbdb().getCurrentUser().is_teacher()) { if (!student_view || !PCBcontext.getPcbdb().getCurrentUser().is_teacher()) {
PCBcontext.getPcbdb().getCurrentUser().get_Img_sup().update_id(student_view ? PCBcontext.getDevice().getLastSupId() : User.NO_SUPERVISOR); PCBcontext.getPcbdb().getCurrentUser().get_Img_sup().update_id(student_view ? PCBcontext.getDevice().getLastSupId() : User.NO_SUPERVISOR);
nextActivity = intent; nextActivity = intent;
} else { } else
PCBcontext.getPcbdb().getCurrentUser().get_Img_sup().update_id(PCBcontext.getDevice().getLastSupId()); if (!PCBcontext.getNetService().online())
nextActivity = new Intent(this, SessionActivity.class); GUITools.show_alert(PictogramActivity.this, R.string.session_noinet);
} else {
PCBcontext.getPcbdb().getCurrentUser().get_Img_sup().update_id(PCBcontext.getDevice().getLastSupId());
nextActivity = new Intent(this, SessionActivity.class);
}
in = R.anim.rightin; in = R.anim.rightin;
out = R.anim.rightout; out = R.anim.rightout;
overridePendingTransition(R.anim.leftin, R.anim.leftout); overridePendingTransition(R.anim.leftin, R.anim.leftout);
} else if (firstTouchX > event.getX() + 150) { //derecha a izquierda } else if (firstTouchX > event.getX() + 150) { //derecha a izquierda
if (!student_view && PCBcontext.getPcbdb().getCurrentUser().is_teacher() ) { if (!student_view && PCBcontext.getPcbdb().getCurrentUser().is_teacher() ) {
nextActivity = new Intent(this, SessionActivity.class); if (!PCBcontext.getNetService().online())
GUITools.show_alert(PictogramActivity.this, R.string.session_noinet);
else
nextActivity = new Intent(this, SessionActivity.class);
} else { } else {
PCBcontext.getPcbdb().getCurrentUser().get_Img_sup().update_id(student_view ? PCBcontext.getDevice().getLastSupId() : User.NO_SUPERVISOR); PCBcontext.getPcbdb().getCurrentUser().get_Img_sup().update_id(student_view ? PCBcontext.getDevice().getLastSupId() : User.NO_SUPERVISOR);
nextActivity = intent; nextActivity = intent;
...@@ -1098,7 +1108,7 @@ protected void showOnlyTape(boolean onlyTape) { ...@@ -1098,7 +1108,7 @@ protected void showOnlyTape(boolean onlyTape) {
case CAMERA_PIC_REQUEST: //Captura de foto case CAMERA_PIC_REQUEST: //Captura de foto
if (data != null && resultCode==RESULT_OK) { if (data != null && resultCode==RESULT_OK) {
imagen = (Bitmap) data.getExtras().get("data"); imagen = (Bitmap) data.getExtras().get("data");
this.launchActivity(imagen); this.launchActivity(imagen,CAMERA_PIC_REQUEST);
//Log.i("DETALLES", "Llega el intent del menú, con texto: " + legend); //Log.i("DETALLES", "Llega el intent del menú, con texto: " + legend);
} else { } else {
if(resultCode!=RESULT_OK) //Si no quieres la foto que has echado if(resultCode!=RESULT_OK) //Si no quieres la foto que has echado
...@@ -1122,7 +1132,7 @@ protected void showOnlyTape(boolean onlyTape) { ...@@ -1122,7 +1132,7 @@ protected void showOnlyTape(boolean onlyTape) {
imagen = BitmapFactory.decodeFile(filePath); imagen = BitmapFactory.decodeFile(filePath);
/** Tras echar foto llamar a la actividad de recortar y le paso la leyenda para si tiene anteriormente o null, y la imagen a recortar */ /** Tras echar foto llamar a la actividad de recortar y le paso la leyenda para si tiene anteriormente o null, y la imagen a recortar */
//-->GERMAN: legend != null ? legend : null es lo mismo que simplemente legend //-->GERMAN: legend != null ? legend : null es lo mismo que simplemente legend
this.launchActivity(imagen); this.launchActivity(imagen,GALLERY_PIC_REQUEST);
}else{ }else{
startActivity(new Intent(this, PictogramActivity.class)); startActivity(new Intent(this, PictogramActivity.class));
} }
...@@ -1138,9 +1148,11 @@ protected void showOnlyTape(boolean onlyTape) { ...@@ -1138,9 +1148,11 @@ protected void showOnlyTape(boolean onlyTape) {
/**Para cambiar la activity de PictogramActivity a EditPictoActivity /**Para cambiar la activity de PictogramActivity a EditPictoActivity
* @param image * @param image
*/ */
public void launchActivity(Bitmap image){ public void launchActivity(Bitmap image, int originImage){
Intent intent = new Intent(this, EditPictoActivity.class); Intent intent = new Intent(this, EditPictoActivity.class);
if(image!=null) { if(image!=null) {
float aspectFactor = 1; float aspectFactor = 1;
float bWidth = image.getWidth(); float bWidth = image.getWidth();
...@@ -1167,6 +1179,9 @@ protected void showOnlyTape(boolean onlyTape) { ...@@ -1167,6 +1179,9 @@ protected void showOnlyTape(boolean onlyTape) {
byte[] byteArray = stream.toByteArray(); byte[] byteArray = stream.toByteArray();
intent.putExtra(EditPictoActivity.IMAGE_PICTO, byteArray); intent.putExtra(EditPictoActivity.IMAGE_PICTO, byteArray);
intent.putExtra(EditPictoActivity.IMAGE_ORIGIN, originImage == CAMERA_PIC_REQUEST ? CAMERA_PIC_REQUEST : GALLERY_PIC_REQUEST);
intent.putExtra(EditPictoActivity.IMAGE_ORIENTATION, bWidth > bHeight ? HORIZONTAL_ORIENTATION : VERTICAL_ORIENTATION);
if(picto!=null) { if(picto!=null) {
intent.putExtra(EditPictoActivity.TRANSCRIPTION, picto.get_translation()); intent.putExtra(EditPictoActivity.TRANSCRIPTION, picto.get_translation());
picto = null; picto = null;
......
package com.yottacode.pictogram.tabletlibrary.net; package com.yottacode.pictogram.tabletlibrary.net;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent; import android.content.Intent;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.util.Log; import android.util.Log;
import com.yottacode.pictogram.dao.User; import com.yottacode.pictogram.dao.User;
...@@ -22,11 +24,12 @@ public class NetServiceTablet implements NetService.iNetServiceDevice { ...@@ -22,11 +24,12 @@ public class NetServiceTablet implements NetService.iNetServiceDevice {
private static NotificationCompat.Builder builder; private static NotificationCompat.Builder builder;
private PictogramActivity pictogramActivity; private PictogramActivity pictogramActivity;
int notifyID = 666;
public void build() { public void build() {
this.builder = new NotificationCompat.Builder(PCBcontext.getContext()); this.builder = new NotificationCompat.Builder(PCBcontext.getContext()).setAutoCancel(true);
/* Intent resultIntent = new Intent(PCBcontext.getContext(), PictogramActivity.class); /*Intent resultIntent = new Intent(PCBcontext.getContext(), PictogramActivity.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(PCBcontext.getContext()); TaskStackBuilder stackBuilder = TaskStackBuilder.create(PCBcontext.getContext());
stackBuilder.addParentStack(PictogramActivity.class); stackBuilder.addParentStack(PictogramActivity.class);
stackBuilder.addNextIntent(resultIntent); stackBuilder.addNextIntent(resultIntent);
...@@ -36,8 +39,8 @@ public class NetServiceTablet implements NetService.iNetServiceDevice { ...@@ -36,8 +39,8 @@ public class NetServiceTablet implements NetService.iNetServiceDevice {
} }
public void notifyStatus(boolean updated) { public void notifyStatus(boolean updated) {
int notifyID = 1;
String user=""; String user="";
if (PCBcontext.getPcbdb()!=null) { if (PCBcontext.getPcbdb()!=null) {
user=PCBcontext.getPcbdb().getCurrentUser().get_name_stu()+" "+PCBcontext.getPcbdb().getCurrentUser().get_surname_stu(); user=PCBcontext.getPcbdb().getCurrentUser().get_name_stu()+" "+PCBcontext.getPcbdb().getCurrentUser().get_surname_stu();
if (PCBcontext.getPcbdb().getCurrentUser().is_supervisor()) if (PCBcontext.getPcbdb().getCurrentUser().is_supervisor())
...@@ -57,7 +60,6 @@ public class NetServiceTablet implements NetService.iNetServiceDevice { ...@@ -57,7 +60,6 @@ public class NetServiceTablet implements NetService.iNetServiceDevice {
mNotificationManager.notify(notifyID, builder.build()); mNotificationManager.notify(notifyID, builder.build());
} }
public void closeNotifyStatus(){ public void closeNotifyStatus(){
int notifyID = 1;
NotificationManager mNotificationManager = NotificationManager mNotificationManager =
(NotificationManager) PCBcontext.getContext().getSystemService(PCBcontext.getContext().NOTIFICATION_SERVICE); (NotificationManager) PCBcontext.getContext().getSystemService(PCBcontext.getContext().NOTIFICATION_SERVICE);
mNotificationManager.cancel(notifyID); mNotificationManager.cancel(notifyID);
......
...@@ -64,14 +64,13 @@ ...@@ -64,14 +64,13 @@
android:orientation="horizontal" android:orientation="horizontal"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:scaleType="centerInside" android:scaleType="centerInside"
android:layout_marginLeft="5dp"
android:maxWidth="700px"
android:maxHeight="350px"
android:layout_below="@+id/title" android:layout_below="@+id/title"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_marginTop="55dp"
android:minHeight="125dp" android:layout_gravity="center_horizontal"
android:layout_gravity="center_horizontal" /> android:maxHeight="350px"
android:maxWidth="700px" />
<EditText <EditText
android:layout_width="wrap_content" android:layout_width="wrap_content"
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
<string name="session_closed_ok">Sesión grabada correctamente. Hora</string> <string name="session_closed_ok">Sesión grabada correctamente. Hora</string>
<string name="session_closed_fail">Sesión no cerrada. Por favor cierre la sesión en el panel de control de Pictogram Tablet</string> <string name="session_closed_fail">Sesión no cerrada. Por favor cierre la sesión en el panel de control de Pictogram Tablet</string>
<string name="session_pause_error">Error pausando la sesión</string> <string name="session_pause_error">Error pausando la sesión</string>
<string name="session_noinet">No hay conexión con el servidor. Por favor, asegúrese que tiene conexión a Internet</string> <string name="session_noinet">Conexión a Internet necesaria para grabar sesiones. Por favor, asegúrese que tiene conexión</string>
<string name="session_inetok">Conexión con el servidor restablecida</string> <string name="session_inetok">Conexión con el servidor restablecida</string>
<string name="session_log_startingsession">iniciando sesión</string> <string name="session_log_startingsession">iniciando sesión</string>
<string name="session_log_closingsession">cerrando sesión</string> <string name="session_log_closingsession">cerrando sesión</string>
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
<string name="session_closed_ok">Sesión grabada correctamente</string> <string name="session_closed_ok">Sesión grabada correctamente</string>
<string name="session_closed_fail">Sesión no cerrada. Por favor intente cerrarla desde</string> <string name="session_closed_fail">Sesión no cerrada. Por favor intente cerrarla desde</string>
<string name="session_pause_error">Error pausando la sesión</string> <string name="session_pause_error">Error pausando la sesión</string>
<string name="session_noinet">No hay conexión con el servidor. Por favor, asegúrese que tiene conexión a Internet</string> <string name="session_noinet">Conexión a Internet necesaria para grabar sesiones. Por favor, asegúrese que tiene conexión</string>
<string name="session_inetok">Conexión con el servidor restablecida</string> <string name="session_inetok">Conexión con el servidor restablecida</string>
<string name="session_log_startingsession">iniciando sesión</string> <string name="session_log_startingsession">iniciando sesión</string>
<string name="session_log_closingsession">cerrando sesión</string> <string name="session_log_closingsession">cerrando sesión</string>
......
...@@ -22,6 +22,7 @@ public class AppContext extends Application { ...@@ -22,6 +22,7 @@ public class AppContext extends Application {
instance = this; instance = this;
registerKioskModeScreenOffReceiver(); registerKioskModeScreenOffReceiver();
startKioskService(); // Service for restarting the app when another app go to foreground startKioskService(); // Service for restarting the app when another app go to foreground
} }
private void registerKioskModeScreenOffReceiver() { private void registerKioskModeScreenOffReceiver() {
......
...@@ -366,6 +366,7 @@ CREATE TABLE IF NOT EXISTS `supervisor` ( ...@@ -366,6 +366,7 @@ CREATE TABLE IF NOT EXISTS `supervisor` (
`gender` char(1) COLLATE utf8_unicode_ci NOT NULL, `gender` char(1) COLLATE utf8_unicode_ci NOT NULL,
`pic` varchar(255) COLLATE utf8_unicode_ci DEFAULT 'defaultAvatar.jpg', `pic` varchar(255) COLLATE utf8_unicode_ci DEFAULT 'defaultAvatar.jpg',
`address` varchar(180) COLLATE utf8_unicode_ci DEFAULT NULL, `address` varchar(180) COLLATE utf8_unicode_ci DEFAULT NULL,
`postal_code` char(10) COLLATE utf8_unicode_ci NOT NULL,
`country` varchar(2) COLLATE utf8_unicode_ci DEFAULT NULL, `country` varchar(2) COLLATE utf8_unicode_ci DEFAULT NULL,
`email` varchar(80) COLLATE utf8_unicode_ci NOT NULL, `email` varchar(80) COLLATE utf8_unicode_ci NOT NULL,
`phone` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL, `phone` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL,
......
# Changes
Changes to be performed manually in servers to upgrade Changes to be performed manually in servers to upgrade
AngularJS ## AngularJS
(already done in dev)
- angular-re-captcha has been replaced by angular-recaptcha, so bower has to be run - angular-re-captcha has been replaced by angular-recaptcha, so bower has to be run
- reinstall ui-bootstrap - reinstall ui-bootstrap
- replace angular-file-upload by ng-file-upload - replace angular-file-upload by ng-file-upload
`bower install`
## Database
Database (already done in dev)
- reload trigers-enrolments-integrity-constraints.sql - reload trigers-enrolments-integrity-constraints.sql
- alter table supervisor to add postal_code:
`alter table supervisor add column `postal_code` char(10) COLLATE utf8_unicode_ci NOT NULL;`
- alter table office to add postal_code:
`alter table office add column `postal_code` char(10) COLLATE utf8_unicode_ci NOT NULL;`
- remove max_students and current_students columns from offices:
`alter table office drop column max_students;`
`alter table office drop column current_students;`
- copy postal_code value from office to its supervisors
`update supervisor as sup inner join office as off on off.id = sup.id_off set sup.postal_code = off.postal_code;`
- alter table office
`alter table office modify logo_url varchar(240) default null;`
...@@ -102,7 +102,8 @@ module.exports = { ...@@ -102,7 +102,8 @@ module.exports = {
*/ */
getAll: function (req, res) { getAll: function (req, res) {
Office.find().populate('admin').then(function (offices) { Office.find().populate('admin').then(function (offices) {
res.ok(offices); // return all offices filtering out personal ones
res.ok(offices.filter(o => o.name !== 'no_office'));
}) })
.catch(function () { .catch(function () {
res.serverError(); res.serverError();
......
...@@ -71,10 +71,13 @@ module.exports = { ...@@ -71,10 +71,13 @@ module.exports = {
Supervisor.findOneByEmail(email).then(function (supervisor) { Supervisor.findOneByEmail(email).then(function (supervisor) {
if (!supervisor) if (!supervisor)
return res.notFound("Supervisor not found") throw new Error("Supervisor not found")
if (!bcrypt.compareSync(password, supervisor.password)) if (!bcrypt.compareSync(password, supervisor.password))
throw res.unauthorized("Invalid email/password") throw new Error("Invalid email/password");
if (supervisor.active == 0)
throw new Error("This account has not been activated");
return (supervisor); return (supervisor);
...@@ -100,7 +103,7 @@ module.exports = { ...@@ -100,7 +103,7 @@ module.exports = {
} }
if (!supervisor.isSupAdmin && !stuSup) if (!supervisor.isSupAdmin && !stuSup)
throw res.unauthorized("Supervisor without students"); throw new Error("Supervisor without students");
return res.ok({ return res.ok({
user: supervisor, user: supervisor,
...@@ -108,9 +111,8 @@ module.exports = { ...@@ -108,9 +111,8 @@ module.exports = {
token: sailsTokenAuth.issueToken(supervisor, sails.config.jwt.expiresInMinutes) token: sailsTokenAuth.issueToken(supervisor, sails.config.jwt.expiresInMinutes)
}); });
}) })
.catch(function (err) { .catch(function (err) {
return res.serverError("Error when connecting to database"); return res.badRequest(err.message);
}); });
}, },
...@@ -119,7 +121,7 @@ module.exports = { ...@@ -119,7 +121,7 @@ module.exports = {
* Activate the user account specified * Activate the user account specified
* @param {request} req * @param {request} req
* { * {
* "token": "12398123aas78sf798as7d987234" // Encryted code with supervisor ID * "token": "12398123aas78sf798as7d987234" // Encryted code with supervisor ID, siging role and id_off
* } * }
* @param {response} login view * @param {response} login view
* { * {
...@@ -132,21 +134,67 @@ module.exports = { ...@@ -132,21 +134,67 @@ module.exports = {
* } * }
*/ */
activate: function (req, res) { activate: function (req, res) {
if (!req.params.token) if (!req.params.token)
return res.badRequest("Invalid activation URL"); return res.badRequest("Invalid activation URL");
sailsTokenAuth.verifyToken(req.params.token, function(err, token) { sailsTokenAuth.verifyToken(req.params.token, function(err, token) {
if (err) if (err)
return res.badRequest("Invalid token"); return res.badRequest("Invalid token");
Supervisor.findOne(token).then(function (supervisor) {
Supervisor.findOne(token.id_sup)
.then(function (supervisor) {
if (!supervisor) if (!supervisor)
throw new Error("Error when looking for user"); throw new Error("Error when looking for user");
supervisor.active = true; supervisor.active = true;
delete supervisor.password; delete supervisor.password;
supervisor.save(); supervisor.save();
return res.view('accountActivated', {sup: supervisor, login_url: 'https://' + req.headers.host + '/app'});
// an email has to be sent to office administrators
if (token.role == 'tutor_office' || token.role === 'therapist_office') {
Office.findOne(token.id_off)
.populate('admin')
.then((off) => {
if (!off)
throw new Error("Office not found: ");
var message = sails.__({
phrase: token.role + '_request',
locale: supervisor.lang
}, {name: supervisor.name + " " + supervisor.surname, email: supervisor.email});
mailService.mailer()
.send({
to: off.admin.email,
text: message
})
.then(() => {})
.catch((err) => {throw err});
})
.catch((err) => {throw err;});
}
// welcome message is returned
return res.view('accountActivated', {
welcome_msg1: sails.__({
phrase: 'welcome_msg1',
locale: supervisor.lang
}, {name: supervisor.name}),
welcome_msg2: sails.__({
phrase: 'welcome_msg2',
locale: supervisor.lang
}),
login_url: 'https://' + req.headers.host + '/app',
login: sails.__('login')
});
}) })
.catch(function (err) { .catch(function (err) {
return res.serverError("Error when activating account " + err); return res.serverError(err.message ? err.message : 'Supervisor not found');
}); });
}); });
}, },
...@@ -212,61 +260,97 @@ module.exports = { ...@@ -212,61 +260,97 @@ module.exports = {
*/ */
create: function (req, res) { create: function (req, res) {
var params = req.params.all(); var params = req.params.all();
var supervisor;
// Send email confirmation
function sendConfirmationMail(cb) {
console.log("mail------------\n" + JSON.stringify(supervisor));
var token = sailsTokenAuth.issueToken({
id_sup: supervisor.id,
role: params.role,
id_off: params.id_off,
}, 60*24*7); // expires in 1 week
var message = sails.__({
phrase: 'signin_mail',
locale: params.lang || 'es-es'
}) + 'https://' + req.headers.host + '/sup/activate/' + token; // expires in 1 week
sails.log.debug("Sending activation email: \n" + message);
mailService.mailer()
.send({
to: params.email,
text: message
})
.then(() => {cb();})
.catch((err) => {cb(err);});
} // /sendConfirmationEmail()
sails.log.debug("Creating supervisor with params " + JSON.stringify(params)); sails.log.debug("Creating supervisor with params " + JSON.stringify(params));
if (params.name && if (!params.name || !params.surname || !params.gender || !params.password || !params.email )
params.surname && res.badRequest("Invalid params");
params.gender &&
params.password &&
params.email) {
Supervisor.create({
name: params.name,
surname: params.surname,
gender: params.gender,
password: params.password,
email: params.email,
pic: sails.config.pictogram.paths.defaultAvatarFileName,
address: params.address || null,
country: params.country || null,
phone: params.phone || null,
lang: params.lang || null,
ttsEngine: params.ttsEngine || null
}).then(function (supervisor) {
if (!supervisor) var supData = {
res.serverError("Supervisor created but returned null"); name: params.name,
surname: params.surname,
gender: params.gender,
password: params.password,
email: params.email,
pic: sails.config.pictogram.paths.defaultAvatarFileName,
address: params.address || '',
postalCode: params.postalCode || '',
country: params.country || '',
phone: params.phone || '',
lang: params.lang || 'es-es',
};
if (params.id_off)
supData.id_off = params.id_off;
console.log("supData:\n" + JSON.stringify(supData));
Supervisor.create(supData)
.then(function (sup) {
sails.log.debug("SUPERVISOR: " + JSON.stringify(supervisor)); if (!sup)
res.serverError("Supervisor created but returned null");
/* Send email confirmation */ supervisor = sup;
var message = sails.__({
phrase: 'signin_mail',
locale: params.lang || 'es-es'
}) + 'https://' + req.headers.host + '/sup/activate/' + sailsTokenAuth.issueToken(supervisor.id, 60*24*7); // expires in 1 week
sails.log.debug("Sending activation email: \n" + message);
mailService.mailer() if (params.role === 'therapist_office' || params.role === 'tutor_office') {
.send({ sendConfirmationMail((err) => {
to: params.email, if (err) throw err;
text: message return res.ok();
})
.then(() => {
res.ok({
user: supervisor,
token: sailsTokenAuth.issueToken(supervisor.id)
});
})
.catch((err) => {
res.serverError("Mail could not be sent " + err);
}); });
} else if (params.role === 'therapist_nooffice' || params.role === 'tutor_nooffice') {
}).catch(function (err) { Office.create(params.office)
res.serverError("Supervisor could not be created: " + err); .then((off) => {
});
} else { // link supervisor with office
res.badRequest("Invalid params"); supervisor.id_off = off.id;
} delete supervisor.password;
supervisor.save();
// set supervisor as admin in the office
off.admin = supervisor.id;
off.save();
sendConfirmationMail((err) => {
if (err) throw err;
return res.ok();
});
})
.catch(err => {throw err});
} else
return res.badRequest("Invalid role");
}).catch(function (err) {
return res.serverError("Supervisor could not be created: " + err);
});
}, },
/* /*
......
...@@ -26,15 +26,25 @@ module.exports = { ...@@ -26,15 +26,25 @@ module.exports = {
type: "string", type: "string",
size: 80 size: 80
}, },
logoUrl: {
columnName: 'logo_url',
type: "string",
size: 240
},
address: { address: {
required: true, required: true,
type: "string", type: "string",
size: 80 size: 80
}, },
logo_url: { country: {
required: true, type: "string",
type: "string", size: 5,
size: 240 required: true
},
lang: {
required: true,
type: "string",
size: 2
}, },
contactPerson: { contactPerson: {
columnName: "contact_person", columnName: "contact_person",
...@@ -57,37 +67,33 @@ module.exports = { ...@@ -57,37 +67,33 @@ module.exports = {
type: "string", type: "string",
size: 20 size: 20
}, },
lang: { admin: {
required: true, columnName: 'admin',
type: "string", type: 'integer',
size: 2 model: 'Supervisor'
}, },
country: { postalCode: {
columnName: 'postal_code',
required: true,
type: "string", type: "string",
size: 5 size: 10
},
maxStudents: {
columnName: "max_students",
type: "integer"
},
currentStudents: {
columnName: "current_students",
type: "integer"
},
admin: {
model: 'supervisor',
required: false
//type: 'integer'
}, },
// Relación con Teacher. [1 Office to N Teacher] // Relación con Teacher. [1 Office to N Teacher]
supervisors: { supervisors: {
collection: "Supervisor", collection: "Supervisor",
via: 'office' via: 'office'
}, },
// Relación con Student. [1 Office to N Student] // Relación con Student. [1 Office to N Student]
students: { students: {
collection: "Student", collection: "Student",
via: 'office' via: 'office'
} }
},
beforeCreate: function (attrs, next) {
if (!attrs.logoUrl)
attrs.logoUrl = '/app/img/logo_pictogram.png';
next();
} }
}; };
...@@ -52,6 +52,11 @@ module.exports = { ...@@ -52,6 +52,11 @@ module.exports = {
type: "string", type: "string",
size: 180 size: 180
}, },
postalCode: {
columnName: 'postal_code',
type: "string",
size: 10
},
country: { country: {
type: "string", type: "string",
size: 2 size: 2
......
...@@ -63,6 +63,8 @@ ...@@ -63,6 +63,8 @@
"contact_person": "Contact person", "contact_person": "Contact person",
"continue_session": "Resume_session", "continue_session": "Resume_session",
"country": "Country", "country": "Country",
"country_requested": "Country requested",
"country_office_requested": "Country for office/center is mandatory",
"create_account": "Create account", "create_account": "Create account",
"create_an_account": "Create an account", "create_an_account": "Create an account",
"credentials": "Credentials", "credentials": "Credentials",
...@@ -136,6 +138,7 @@ ...@@ -136,6 +138,7 @@
"highlighted": "Highlighted", "highlighted": "Highlighted",
"hours": "hours", "hours": "hours",
"how_many": "How many?", "how_many": "How many?",
"inactive_account": "This account has not been activated. Please, follow the link sent to your email address.",
"input_selection": "How to place a pictogram", "input_selection": "How to place a pictogram",
"instruction": "Instruction", "instruction": "Instruction",
"instruction_begin": "Begin instruction", "instruction_begin": "Begin instruction",
...@@ -204,6 +207,7 @@ ...@@ -204,6 +207,7 @@
"no": "No", "no": "No",
"nobegin": "No started", "nobegin": "No started",
"no_method": "No method defined", "no_method": "No method defined",
"no_office": "No office",
"no_subscribed": "No connection to student account", "no_subscribed": "No connection to student account",
"no_instruction": "No instruction defined", "no_instruction": "No instruction defined",
"no_students_for_user": "You are not associated to any students. Please ask your office to link your account to a Pictogram student.", "no_students_for_user": "You are not associated to any students. Please ask your office to link your account to a Pictogram student.",
......
...@@ -63,6 +63,8 @@ ...@@ -63,6 +63,8 @@
"contact_person": "Persona de contacto", "contact_person": "Persona de contacto",
"continue_session": "Reanudar sesión", "continue_session": "Reanudar sesión",
"country": "País", "country": "País",
"country_requested": "Debe especificar el país",
"country_office_requested": "Debe especificar el país del gabinete/centro",
"create_account": "Crear cuenta", "create_account": "Crear cuenta",
"create_an_account": "Crear una cuenta", "create_an_account": "Crear una cuenta",
"credentials": "Credenciales", "credentials": "Credenciales",
...@@ -136,6 +138,7 @@ ...@@ -136,6 +138,7 @@
"highlighted": "Resaltado", "highlighted": "Resaltado",
"hours": "horas", "hours": "horas",
"how_many": "¿Cuántas?", "how_many": "¿Cuántas?",
"inactive_account": "Esta cuenta no ha sido activada. Por favor, siga el enlace de confirmación enviado a su dirección de correo electrónico.",
"input_selection": "Cómo colocar un pictograma", "input_selection": "Cómo colocar un pictograma",
"instruction": "Instrucción", "instruction": "Instrucción",
"instruction_begin": "Primer intento", "instruction_begin": "Primer intento",
...@@ -203,6 +206,7 @@ ...@@ -203,6 +206,7 @@
"next_sessions": "Sesiones posteriores", "next_sessions": "Sesiones posteriores",
"no": "No", "no": "No",
"no_method": "Método sin definir", "no_method": "Método sin definir",
"no_office": "Sin centro",
"no_instruction": "Instrucción sin definir", "no_instruction": "Instrucción sin definir",
"no_students_for_user": "Su cuenta no está asociada a ningún estudiante. Por favor, contacte con su gabinete para enlazar su cuenta a un estudiante.", "no_students_for_user": "Su cuenta no está asociada a ningún estudiante. Por favor, contacte con su gabinete para enlazar su cuenta a un estudiante.",
"no_space_in_category": "No queda espacio en la categoría", "no_space_in_category": "No queda espacio en la categoría",
......
...@@ -24,7 +24,7 @@ function LoginCtrl( ...@@ -24,7 +24,7 @@ function LoginCtrl(
}; };
$scope.office = { $scope.office = {
logo_url : 'img/logo_pictogram.png', logoUrl : 'img/logo_pictogram.png',
name : 'Pictogram' name : 'Pictogram'
}; };
...@@ -48,18 +48,23 @@ function LoginCtrl( ...@@ -48,18 +48,23 @@ function LoginCtrl(
data.user.isTutor = true; data.user.isTutor = true;
} else } else
data.user.isTutor = false; data.user.isTutor = false;
if (data.user.office.logo_url.length < 5) if (data.user.office.logoUrl.length < 5)
data.user.office.logo_url = 'img/logo_pictogram.png'; data.user.office.logoUrl = 'img/logo_pictogram.png';
$window.sessionStorage.token = data.token; $window.sessionStorage.token = data.token;
// Adapt language en-us to en-gb (the latter is the one supported for 'en') //User data correct
if (data.user) { if (data.user) {
// Adapt language en-us to en-gb (the latter is the one supported for 'en')
if (data.user.lang === 'en-us') { if (data.user.lang === 'en-us') {
data.user.lang = 'en-gb'; data.user.lang = 'en-gb';
} }
//Update $scope
$scope.lang = data.user.lang; $scope.lang = data.user.lang;
//Update $translate
$translate.use($scope.lang);
} else { } else {
//No user data, use default lang
$translate.use($scope.lang); $translate.use($scope.lang);
} }
...@@ -83,8 +88,8 @@ function LoginCtrl( ...@@ -83,8 +88,8 @@ function LoginCtrl(
$translate('no_students_for_user').then(function (translation) { $translate('no_students_for_user').then(function (translation) {
ngToast.warning({ content: translation }); ngToast.warning({ content: translation });
}); });
} else { } else if (err.search("not been activated") > 0) {
$translate('login_fail').then(function (translation) { $translate('inactive_account').then(function (translation) {
ngToast.danger({ content: translation }); ngToast.danger({ content: translation });
}); });
} }
......
...@@ -21,16 +21,25 @@ function SignInCtrl($scope, ...@@ -21,16 +21,25 @@ function SignInCtrl($scope,
name: '', name: '',
surname: '', surname: '',
address: '', address: '',
postal_code: '',
country: '00',
phone: '', phone: '',
gender: 'M', gender: 'F',
email: '', email: '',
email_confirm: '', email_confirm: '',
password: '', password: '',
password_confirm: '', password_confirm: '',
lang: 'es', lang: '00',
role: null, role: 'therapist_office',
office: { office: {
country: 'NULL' name: '',
address: '',
postalCode: '',
country: '00',
contactPerson: '',
phone1: '',
email: '',
logoUrl: ''
}, },
office_idx: -1 office_idx: -1
}; };
...@@ -48,6 +57,17 @@ function SignInCtrl($scope, ...@@ -48,6 +57,17 @@ function SignInCtrl($scope,
ngToast.danger({ content: $translate.instant('server_error') }); ngToast.danger({ content: $translate.instant('server_error') });
}); });
// Copy fields from supervisor to office
$scope.copyFields = function () {
$scope.formdata.office.address = $scope.formdata.address;
$scope.formdata.office.postalCode = $scope.formdata.postalCode;
$scope.formdata.office.country = $scope.formdata.country;
$scope.formdata.office.lang = $scope.formdata.lang;
$scope.formdata.office.email = $scope.formdata.email;
$scope.formdata.office.phone1 = $scope.formdata.phone;
$scope.formdata.office.contactPerson = $scope.formdata.name + " " + $scope.formdata.surname;
};
// Form submit // Form submit
$scope.signin = function () { $scope.signin = function () {
// Validate email match // Validate email match
...@@ -67,6 +87,16 @@ function SignInCtrl($scope, ...@@ -67,6 +87,16 @@ function SignInCtrl($scope,
return; return;
} }
if ($scope.formdata.country == '00') {
ngToast.danger({ content: $translate.instant('country_requested') });
return;
}
if ($scope.formdata.role == 'therapist_nooffice' && $scope.formdata.office.country == '00') {
ngToast.danger({ content: $translate.instant('country_office_requested') });
return;
}
if (!$scope.formdata.disclaimer_accepted) { if (!$scope.formdata.disclaimer_accepted) {
ngToast.danger({ content: $translate.instant('disclaimer_requested') }); ngToast.danger({ content: $translate.instant('disclaimer_requested') });
return; return;
...@@ -82,10 +112,26 @@ function SignInCtrl($scope, ...@@ -82,10 +112,26 @@ function SignInCtrl($scope,
$scope.showdialog = true; $scope.showdialog = true;
if ($scope.formdata.office_idx != -1) {
$scope.formdata.id_off = $scope.offices[$scope.formdata.office_idx].id;
delete $scope.formdata.office_idx;
}
if ($scope.formdata.role === 'tutor_nooffice') {
$scope.formdata.office.name = 'no_office';
$scope.formdata.office.address = $scope.formdata.address;
$scope.formdata.office.postalCode = $scope.formdata.postalCode;
$scope.formdata.office.country = $scope.formdata.country;
$scope.formdata.office.contactPerson = $scope.formdata.name + " " + $scope.formdata.surname;
$scope.formdata.office.phone1 = $scope.formdata.phone;
$scope.formdata.office.email = $scope.formdata.email;
$scope.formdata.office.lang = $scope.formdata.lang;
}
$http $http
.post(config.backend + '/sup', $scope.formdata) .post(config.backend + '/sup', $scope.formdata)
.success(function () { .success(function () {
ngToast.success({ content: $translate.instant('user_created') }); ngToast.success({ content: $translate.instant('user_created', { name: $scope.formdata.name, surname: $scope.formdata.surname }) });
$scope.reset(); $scope.reset();
}) })
.error(function () { .error(function () {
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
</p> </p>
--> -->
<p class="text-center"> <p class="text-center">
<img ng-src="{{office.logo_url}}" alt="{{office.name}}" title="{{office.name}}"> <img ng-src="{{office.logoUrl}}" alt="{{office.name}}" title="{{office.name}}">
</p> </p>
<!-- Formulario --> <!-- Formulario -->
<!-- LoginCtrl controls here, see app.js --> <!-- LoginCtrl controls here, see app.js -->
......
...@@ -33,7 +33,25 @@ ...@@ -33,7 +33,25 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" id="signin_address" placeholder="{{ 'address' | translate }}" ng-model="formdata.address"/> <input type="text" class="form-control" id="signin_address" placeholder="{{ 'address' | translate }}" ng-model="formdata.address"/ required>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<input type="text" class="form-control" id="signin_postal_code" placeholder="{{ 'postal_code' | translate }}" required ng-model="formdata.postalCode" ng-change="formdata.postalCode = formdata.postalCode.toUpperCase()"/>
</div>
</div>
<div class="col-md-8">
<div class="form-group">
<select class="form-control" ng-model="formdata.country" required>
<option value="00" selected disabled hidden>{{ 'country' | translate }}</option>
<option value="ES">España</option>
<option value="US">United States</option>
<option value="UK">United Kingdom</option>
<option value="IE">Ireland</option>
</select>
</div>
</div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
...@@ -90,9 +108,9 @@ ...@@ -90,9 +108,9 @@
<fieldset> <fieldset>
<fieldset> <fieldset>
<label translate>language</label>
<div class="form-group"> <div class="form-group">
<select class="form-control" name="signin_language" id="signin_language" ng-model="formdata.lang"> <select class="form-control" name="signin_language" id="signin_language" ng-model="formdata.lang" required>
<option value="00" selected disabled hidden>{{ 'language' | translate }}</option>
<option value="es-es" selected>Español</option> <option value="es-es" selected>Español</option>
<option value="en-gb">English</option> <option value="en-gb">English</option>
</select> </select>
...@@ -124,7 +142,8 @@ ...@@ -124,7 +142,8 @@
type="radio" type="radio"
ng-model="formdata.role" ng-model="formdata.role"
value="therapist_nooffice" value="therapist_nooffice"
onClick="$('#office_selection').slideUp(); $('#office_form').slideDown();"> onClick="$('#office_selection').slideUp(); $('#office_form').slideDown();"
ng-click="copyFields();">
</div> </div>
<div class="col-md-11"> <div class="col-md-11">
{{ 'case_therapist_nooffice' | translate }} {{ 'case_therapist_nooffice' | translate }}
...@@ -154,7 +173,8 @@ ...@@ -154,7 +173,8 @@
type="radio" type="radio"
ng-model="formdata.role" ng-model="formdata.role"
value="tutor_nooffice" value="tutor_nooffice"
onClick="$('#office_selection').slideUp(); $('#office_form').slideUp();"> onClick="$('#office_selection').slideUp(); $('#office_form').slideUp(); copyFields();"
ng-click="copyFields();">
</div> </div>
<div class="col-md-11"> <div class="col-md-11">
{{ 'case_tutor_nooffice' | translate }} {{ 'case_tutor_nooffice' | translate }}
...@@ -166,54 +186,45 @@ ...@@ -166,54 +186,45 @@
<div class="form-group" id="office_form" hidden> <div class="form-group" id="office_form" hidden>
<legend translate>office_center</legend> <legend translate>office_center</legend>
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" placeholder="{{ 'name' | translate }}" ng-model="formdata.office.name"/> <input type="text" class="form-control" placeholder="{{ 'name' | translate }}" ng-model="formdata.office.name" ng-required="formdata.role == 'therapist_nooffice'"/>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" placeholder="{{ 'logo_url' | translate }}" ng-model="formdata.office.logo_url"/> <input type="text" class="form-control" placeholder="{{ 'logo_url' | translate }}" ng-model="formdata.office.logoUrl"/>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" placeholder="{{ 'address' | translate }}" ng-model="formdata.office.address"/> <input type="text" class="form-control" placeholder="{{ 'address' | translate }}" ng-model="formdata.office.address" ng-required="formdata.role == 'therapist_nooffice'"/>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" placeholder="{{ 'postal_code' | translate }}" ng-model="formdata.office.postal_code"/> <input type="text" class="form-control" placeholder="{{ 'postal_code' | translate }}" ng-model="formdata.office.postalCode" ng-change="formdata.office.postalCode = formdata.office.postalCode.toUpperCase()" ng-required="formdata.role == 'therapist_nooffice'"/>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="form-group">
<select class="form-control" ng-model="formdata.office.country"> <input type="text" class="form-control" placeholder="{{ 'contact_person' | translate }}" ng-model="formdata.office.contactPerson" ng-required="formdata.role == 'therapist_nooffice'"/>
<option value="NULL" selected disabled hidden>{{ 'country' | translate }}</option>
<option value="ES">España</option>
<option value="US">United States</option>
<option value="UK">United Kingdom</option>
<option value="IE">Ireland</option>
</select>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group">
<input type="text" class="form-control" placeholder="{{ 'contact_person' | translate }}" ng-model="formdata.office.contact_person"/>
</div>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="form-group">
<input type="email" class="form-control" placeholder="{{ 'email' | translate }}" ng-model="formdata.office.email"/> <input type="email" class="form-control" placeholder="{{ 'email' | translate }}" ng-model="formdata.office.email" ng-required="formdata.role == 'therapist_nooffice'"/>
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" placeholder="{{ 'phone' | translate }}" ng-model="formdata.office.phone1"/> <input type="text" class="form-control" placeholder="{{ 'phone' | translate }}" ng-model="formdata.office.phone1" ng-required="formdata.role == 'therapist_nooffice'"/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="form-group" id="office_selection" hidden> <div class="form-group" id="office_selection">
<legend translate>office_center</legend> <legend translate>office_center</legend>
<select class="form-control" ng-model="formdata.office_idx"> <select class="form-control" ng-model="formdata.office_idx" ng-required="formdata.role == 'therapist_office' || formdata.role == 'tutor_office'">
<option selected disabled hidden value="-1">{{ 'select_office' | translate }}</option> <option selected disabled hidden value="-1">{{ 'select_office' | translate }}</option>
<option ng-repeat="office in offices | orderBy: 'name' track by $index" value="$index"> {{ office.name }} </option> <option ng-repeat="office in offices | orderBy: 'name' track by $index" ng-value="$index"> {{ office.name }} </option>
</select> </select>
</div> </div>
...@@ -229,6 +240,7 @@ ...@@ -229,6 +240,7 @@
</div> </div>
<p class="text-center"> <p class="text-center">
<a href="/app/#/login" class="btn btn-default" translate>cancel</a>
<button type="submit" class="btn btn-primary" ng-disabled="signInForm.$invalid" translate>create_account</button> <button type="submit" class="btn btn-primary" ng-disabled="signInForm.$invalid" translate>create_account</button>
</p> </p>
......
...@@ -173,7 +173,7 @@ dashboardControllers.controller('StudentSessionCtrl', function StudentSessionCtr ...@@ -173,7 +173,7 @@ dashboardControllers.controller('StudentSessionCtrl', function StudentSessionCtr
function(data, status, headers, config) { function(data, status, headers, config) {
$scope.wsessions[0].end = data.data.end; $scope.wsessions[0].end = data.data.end;
$scope.ws_recover=false; $scope.ws_recover=false;
$sessionRunning = false; $scope.sessionRunning = false;
} }
,function(data, status, headers, config) { ,function(data, status, headers, config) {
} }
......
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
<!-- /.picto-legend --> <!-- /.picto-legend -->
<img <img
src="/app/img/redcross.png" src="/app/img/redcross.png"
class="disabled" class="red-cross-visibility disabled"
ng-if="studentPicto.attributes.status == 'disabled'"/> ng-if="studentPicto.attributes.status == 'disabled'"/>
<img <img
ng-src="{{studentPicto.picto.uri}}" ng-src="{{studentPicto.picto.uri}}"
...@@ -137,7 +137,7 @@ ...@@ -137,7 +137,7 @@
<!-- /.picto-legend --> <!-- /.picto-legend -->
<img <img
src="/app/img/redcross.png" src="/app/img/redcross.png"
class="disabled" class="red-cross-visibility disabled"
ng-if="studentPicto.attributes.status == 'disabled'"/> ng-if="studentPicto.attributes.status == 'disabled'"/>
<img <img
ng-src="{{studentPicto.picto.uri}}" ng-src="{{studentPicto.picto.uri}}"
...@@ -258,7 +258,7 @@ ...@@ -258,7 +258,7 @@
<!-- /.picto-legend --> <!-- /.picto-legend -->
<img <img
src="/app/img/redcross.png" src="/app/img/redcross.png"
class="disabled" class="red-cross-visibility disabled"
ng-if="studentPicto.attributes.status == 'disabled'"/> ng-if="studentPicto.attributes.status == 'disabled'"/>
<img <img
ng-src="{{studentPicto.picto.uri}}" ng-src="{{studentPicto.picto.uri}}"
......
...@@ -111,7 +111,7 @@ ...@@ -111,7 +111,7 @@
<div class="form-group"> <div class="form-group">
<label translate>gender</label> <label translate>gender</label>
<select class="form-control" name="student_gender" id="student_gender" ng-model="formUser.gender" required> <select class="form-control" name="student_gender" id="student_gender" ng-model="formUser.gender" required>
<option value=" ">&nsp;</option> <option value=" " disabled></option>
<option value="F" translate>woman</option> <option value="F" translate>woman</option>
<option value="M" translate>man</option> <option value="M" translate>man</option>
</select> </select>
...@@ -146,7 +146,7 @@ ...@@ -146,7 +146,7 @@
</fieldset> </fieldset>
<div class="form-group text-center"> <div class="form-group text-center">
<button type="submit" class="btn btn-primary" translate>save</button> <button type="submit" class="btn btn-primary" ng-click="updateStudent()" translate>save</button>
</div> </div>
</form> </form>
</div> </div>
......
...@@ -35,9 +35,6 @@ dashboardControllers.controller('StudentsCtrl', function StudentsCtrl( ...@@ -35,9 +35,6 @@ dashboardControllers.controller('StudentsCtrl', function StudentsCtrl(
if ($scope.user.office.admin === $scope.user.id) { if ($scope.user.office.admin === $scope.user.id) {
$scope.user.isAdmin = true; $scope.user.isAdmin = true;
} }
console.log("currentStudents: " + $scope.user.office.currentStudents);
console.log("maxStudents: " + $scope.user.office.maxStudents);
} else { } else {
$scope.user.office = { name: '' }; $scope.user.office = { name: '' };
} }
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
//----------------------- //-----------------------
// Supervisor Controller // Supervisor Controller
//----------------------- //-----------------------
dashboardControllers.controller('SupervisorCtrl', function SupervisorCtrl($scope, $window, $location, IOService, $modal) { dashboardControllers.controller('SupervisorCtrl', function SupervisorCtrl($scope, $window, $location, IOService, $modal, $translate) {
// Restore user data from session // Restore user data from session
var user = JSON.parse($window.sessionStorage.user); var user = JSON.parse($window.sessionStorage.user);
...@@ -16,6 +16,8 @@ dashboardControllers.controller('SupervisorCtrl', function SupervisorCtrl($scope ...@@ -16,6 +16,8 @@ dashboardControllers.controller('SupervisorCtrl', function SupervisorCtrl($scope
$scope.user.surname = user.surname; $scope.user.surname = user.surname;
$scope.user.pic = user.pic; $scope.user.pic = user.pic;
$scope.user.office = user.office; $scope.user.office = user.office;
if ($scope.user.office.name == 'no_office')
$scope.user.office.name = $translate.instant('no_office');
$scope.user.lang = user.lang; $scope.user.lang = user.lang;
$scope.user.isSupAdmin = user.isSupAdmin; $scope.user.isSupAdmin = user.isSupAdmin;
$scope.user.isTutor = user.isTutor; $scope.user.isTutor = user.isTutor;
...@@ -33,61 +35,4 @@ dashboardControllers.controller('SupervisorCtrl', function SupervisorCtrl($scope ...@@ -33,61 +35,4 @@ dashboardControllers.controller('SupervisorCtrl', function SupervisorCtrl($scope
} }
}); });
//
// Own Pictos
//
$scope.own_pictos = function () {
console.log();
var modalInstance = $modal.open({
animation: true,
templateUrl: 'modules/supervisor/views/own_pictos.html',
controller: 'AddPictoCtrl',
size: 'lg',
resolve: {
student: function () {
return $scope.studentData;
},
supervisor: function () {
return $scope.user;
}
}
});
// Returned data from the modal window
modalInstance.result.then(function (pictoId) {
// Send the picto to the server
$http.post(config.backend + '/stu/' + $scope.studentData.id + '/picto/' + pictoId, {
attributes: {
id_cat: $scope.showFreeCategory ? null : $scope.getCategoryId($scope.selectedCategory),
coord_x: $scope.showFreeCategory ? null : col,
coord_y: $scope.showFreeCategory ? null : row,
status: 'enabled',
free_category_coord_x: $scope.showFreeCategory ? col : null,
free_category_coord_y: $scope.showFreeCategory ? row : null
}
})
.success(function (studentPicto) {
console.log(studentPicto);
placePicto(studentPicto);
io.socket.post('/stu/vocabulary', {
action: 'add',
attributes: {
id_stu: $scope.studentData.id,
stu_picto: studentPicto
}
}, function () {});
})
.error(function (err) {
if (err.code && err.code == 1) // codes are in sails/config/pictogram.js
ngToast.danger({ content: $translate.instant('error_duplicated_picto') });
else
ngToast.danger({ content: $translate.instant('error_adding_picto') });
});
// not needed
// $scope.loadPictos();
});
};
}); });
...@@ -5,31 +5,16 @@ ...@@ -5,31 +5,16 @@
//-------------------------- //--------------------------
dashboardControllers.controller('SupervisorsCtrl', function SupervisorsCtrl($scope, $window, $http, config, $translate, ngToast) { dashboardControllers.controller('SupervisorsCtrl', function SupervisorsCtrl($scope, $window, $http, config, $translate, ngToast) {
//Office ID
$http $http
.get(config.backend+'/office/get_all') .get(config.backend+'/office/get/' + $scope.user.office.id + '/supervisors')
.success(function(data, status, headers, config) { .success(function(data, status, headers, config) {
// Add to list $scope.supervisors_list = data;
$scope.office = data[0]; console.log($scope.supervisors_list);
$scope.supervisors_list();
}) })
.error(function(data, status, headers, config) { .error(function(data, status, headers, config) {
console.log("Error from API: " + data.error); $translate('error_downloading_supervisors').then(function (translation) {
ngToast.danger({ content: translation });
});
}); });
// List of supervisors
$scope.supervisors_list = function(){
$http
.get(config.backend+'/office/get/' + $scope.office.id + '/supervisors')
.success(function(data, status, headers, config) {
$scope.supervisors_list = data;
console.log($scope.supervisors_list);
})
.error(function(data, status, headers, config) {
$translate('error_downloading_supervisors').then(function (translation) {
ngToast.danger({ content: translation });
});
});
};
}); });
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
href="/app/#/students"> href="/app/#/students">
<img <img
class="topbar__logo__image" class="topbar__logo__image"
ng-src="{{user.office.logo_url}}" ng-src="{{user.office.logoUrl}}"
alt="{{user.office.name}}" alt="{{user.office.name}}"
title="{{user.office.name}}" /> title="{{user.office.name}}" />
</a> </a>
...@@ -44,12 +44,6 @@ ...@@ -44,12 +44,6 @@
</a> </a>
</li> </li>
<li> <li>
<a ng-click="own_pictos()" class="pointer" role="menuitem" tabindex="0" href="">
<i class="glyphicon glyphicon-picture" aria-hidden="true"></i>
{{ 'own_pictos' | translate }}
</a>
</li>
<li>
<a class="pointer" role="menuitem" tabindex="0" href="/app/#/setup"> <a class="pointer" role="menuitem" tabindex="0" href="/app/#/setup">
<i class="glyphicon glyphicon-cog" aria-hidden="true"></i> <i class="glyphicon glyphicon-cog" aria-hidden="true"></i>
{{ 'setup' | translate }} {{ 'setup' | translate }}
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
</div> </div>
<div class="col-xs-6 input-group"> <div class="col-xs-6 input-group">
<input type="text" ng-model="search_students" id="search_students" placeholder="{{ 'filter' | translate }}" class="form-control" aria-describedby="basic-addon2"> <input type="text" ng-model="search_students" id="search_students" placeholder="{{ 'filter' | translate }}" class="form-control" aria-describedby="basic-addon2">
<span class="input-group-addon glyphicon glyphicon-search" id="basic-addon2" aria-hidden="true"></span> <span class="input-group-addon"><span class="glyphicon glyphicon-search" id="basic-addon2" aria-hidden="true"></span></span>
</div> </div>
<div class="col-xs-3"> <div class="col-xs-3">
</div> </div>
......
...@@ -525,6 +525,11 @@ textarea.editable{ ...@@ -525,6 +525,11 @@ textarea.editable{
justify-content: center; justify-content: center;
} }
/* Red cross in student collection */
.red-cross-visibility {
z-index: 1;
}
/* In addpicto */ /* In addpicto */
#collections{ #collections{
height: 300px; height: 300px;
......
...@@ -3,5 +3,10 @@ ...@@ -3,5 +3,10 @@
"A brand new app.": "A brand new app.", "A brand new app.": "A brand new app.",
"notification_from_pictogram": "Notification from Pictogram", "notification_from_pictogram": "Notification from Pictogram",
"signin_mail": "To activate your Pictogram account, click on this link:\n", "signin_mail": "To activate your Pictogram account, click on this link:\n",
"change_password_mail": "To change your password, please click on the following link:\n" "change_password_mail": "To change your password, please click on the following link:\n",
"login": "login",
"therapist_office_request": "{{ name }}, with email {{ email }}, is requesting to be linked as therapist to any of your students.",
"tutor_office_request": "{{ name }}, with email {{ email }}, is requesting to be linked as tutor/father/mother to any of your students.",
"welcome_msg1": "Welcome to Pictogram, {{ name }}!",
"welcome_msg2": "Your account is now active. You can proceed to"
} }
...@@ -3,5 +3,10 @@ ...@@ -3,5 +3,10 @@
"A brand new app.": "Una aplicación de la nueva marca.", "A brand new app.": "Una aplicación de la nueva marca.",
"notification_from_pictogram": "Notificación desde Pictogram", "notification_from_pictogram": "Notificación desde Pictogram",
"signin_mail": "Para activar su cuenta en Pictogram, haga click en el siguiente enlace:\n", "signin_mail": "Para activar su cuenta en Pictogram, haga click en el siguiente enlace:\n",
"change_password_mail": "Para cambiar su contraseña, haga click en el siguiente enlace:\n" "change_password_mail": "Para cambiar su contraseña, haga click en el siguiente enlace:\n",
"login": "acceder",
"therapist_office_request": "El/la terapeuta {{ name }}, con correo electrónico {{ email }}, pide ser asociado a algún estudiante.",
"tutor_office_request": "El/la tutor/a/padre/madre {{ name }}, con correo electrónico {{ email }}, pide ser asociado a algún estudiante.",
"welcome_msg1": "¡Bienvenido a Pictogram, {{ name }}!",
"welcome_msg2": "Su cuenta está ahora activa, por lo que puede"
} }
...@@ -112,6 +112,9 @@ module.exports.pictogram = { ...@@ -112,6 +112,9 @@ module.exports.pictogram = {
}, },
error_codes: { error_codes: {
'DUPLICATED_PICTO': 1 'DUPLICATED_PICTO': 1,
'OFFICE_NOT_FOUND': 2,
'SUPERVISOR_NOT_FOUND': 3,
'STUDENT_NOT_FOUND': 4
} }
}; };
...@@ -2,21 +2,31 @@ ...@@ -2,21 +2,31 @@
<html lang="es"> <html lang="es">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Pictogram Dashboard</title> <title>Pictogram Web</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="/app/css/main.css"> <link rel="stylesheet" href="/app/css/main.css">
</head> </head>
<body> <body>
<img title="Pictogram" alt="Pictogram" src="/app/img/logo_pictogram.png"> <div class="container">
<div class="row">
<div class="col-md-2">
<img title="Pictogram" alt="Pictogram" src="/app/img/logo_pictogram.png" style="margin-top:40px">
</div>
<div class="col-md-10">
<div class="page-header">
<h1><%= welcome_msg1 %></h1>
</div>
<p class="lead"><%= welcome_msg2 %> <a href="<%= login_url %>"><%= login %></a>.</p>
</div>
</div>
</div>
<p><strong>Welcome to Pictogram, <%= sup.name %>!</strong></p> <footer class="footer">
<p>Your account is now active, so you can proceed to <a href="<%= login_url %>">login</a>.</p> <div class="container">
<p class="text-muted">Pictogram Web - Yottacode S.L.</p>
<p></p> </div>
</footer>
<p><strong>¡Bienvenido a Pictogram, <%= sup.name %>!</strong></p>
<p>Su cuenta está ahora activa, por lo que puede <a href="<%= login_url %>">acceder</a>.</p>
</body> </body>
</html> </html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment